Struct with abstract-type field or with concrete-type Union in Julia?

812 Views Asked by At

I have two concrete types called CreationOperator and AnnihilationOperator and I want to define a new concrete type that represents a string of operators and a real coefficient that multiplies it.

I found natural to define an abstract type FermionicOperator, from which both CreationOperator and AnnihilationOperator inherit, i.e.

abstract type FermionicOperator end

struct CreationOperator <: FermionicOperator
  ...
end

struct AnnihilationOperator <: FermionicOperator
  ...
end

because I can define many functions with signatures of the type function(op1::FermionicOperator, op2::FermionicOperator) = ..., such as arithmetic operations (I am building an algebra system, so I have to define operations such as *, +, etc. on the operators).
Then I would go on and define a concrete type OperatorString

struct OperatorString
  coef::Float64
  ops::Vector{FermionicOperator}
end

However, according to the Julia manual, I believe that OperatorString is not ideal for performance, because the compiler does not know anything about FermionicOperator and thus functions involving OperatorString will be inefficient (and I will have many functions manipulating strings of operators).

I found the following solution, however I am not sure about its implication and if it really makes a difference.
Instead of defining FermionicOperator as an abstract type, I define it is as the Union of CreationOperator and AnnihilationOperator, i.e.

struct CreationOperator
  ...
end

struct AnnihilationOperator
  ...
end

FermionicOperator = Union{CreationOperator,AnnihilationOperator}

This would still allow functions of the form function(op1::FermionicOperator, op2::FermionicOperator) = ..., but at the same time, to my understanding, Union{CreationOperator,AnnihilationOperator} is a concrete type, such that OperatorString is well-defined and the compilers can optimize things if it's the case.

I am particularly in doubt because I also considered to use the built-in Expr struct to define my string of operators (actually it would be more general), whose field args is a vector with abstract-type elements: very similar to my first design attempt. However, while implementing arithmetic operations on Expr I had the feeling I was doing something "wrong" and I was better off defining my own types.

1

There are 1 best solutions below

2
Jeffrey Sarnoff On

If your ops field is a vector that, in any given instance, is either all CreationOperators or all AnnihilationOperators, then the recommended solution is to use a parameterized struct.

abstract type FermionicOperator end

struct CreationOperator <: FermionicOperator
    ...
end

struct AnnihilationOperator <: FermionicOperator
    ...
end

struct OperatorString{T<:FermionicOperator}
  coef::Float64
  ops::Vector{T}

  function OperatorString(coef::Float64, ops::Vector{T}) where {T<:FermionicOperator}
      return new{T}(coef, ops)
  end
end

If your ops field is a vector that, in any given instance, may be a mixture of CreationOperators and AnnihilationOperators, then you can use a Union. Because the union is small (2 types), your code will remain performant.

struct CreationOperator
    value::Int
end

struct AnnihilationOperator
    value::Int
end

const Fermionic = Union{CreationOperator, AnnihilationOperator}

struct OperatorString
  coef::Float64
  ops::Vector{Fermionic}

  function OperatorString(coef::Float64, ops::Vector{Fermionic})
      return new(coef, ops)
  end
end

Although not shown, even with the Union approach, you may want to use the abstract type also -- just for future simplicity and flexibility in function dispatch. It is helpful in developing robust multidispatch-driven logic.