Store complex and real numbers with same underlying

Hi,
I am trying to store multiple values in a struct. I’d like those values to have the same precision (expected use cases are floating point and fixed point types with several precisions). Some of those values may be complex, but don’t have to be.

I am basically trying something like

struct MyData{T<:Number}
	a::T
	b::real(T)
end

or

struct MyData{T} where {T<:Number}
	a::T
	b::real(T)
end

With the first version, I see the error MethodError: no method matching real(::TypeVar), whereas the second version shows me a syntax error in the (here) first line.

The thing is, I do not want to make a a Complex{T} as in this question and be done with it. Further downstream, I have to do differing things for real T and complex T.

Due to this, also something like

struct MyData{T<:Number}
    a::Complex{T}
    b::T
end

f(d::MyData{<:Real}) = √(2π)
f(d::MyData{<:Complex}) = π

is not an option, because I can’t construct a Complex from another Complex, only from Reals.

How can I store real values and probably real, probably complex values with the same underlying type, so that their type (precision as well as real/complex nature) is preserved for later dispatch?

Does this meet your need?

struct MyData{S, T<:Union{S, Complex{S}}}
  a::S  # Real
  b::T  # Real or Complex
end

data1 = MyData(1.0, 2.0)
data2 = MyData(1.0, 2.0+1.0im)
data3 = MyData(1, 2)
data4 = MyData(1, 2-2im)

dispatch(x::MyData{Float64, Float64}) =
  x == data1
dispatch(x::MyData{Float64, Complex{Float64}}) =
  x == data2

dispatch(x::MyData{Int64, T}) where {T<:Union{Int64, Complex{Int64}}} =
  x == data3 || x == data4
# or the last can be done this way
const  MaybeComplexInt64 =
  Union{Int64, Complex{Int64}}
dispatch(x::MyData{Int64, T}) where {T<:MaybeComplexInt64} =
  x == data3 || x == data4

julia> dispatch(data1)
true
julia> dispatch(data2)
true
julia> dispatch(data3)
true
julia> dispatch(data4)
true
1 Like

That seems to work, thanks!

I guess I can make your MaybeComplexInt64 parametric as well?

[re-edited that answer to show both approaches]

For further reference: Yep, this does work.

const PossiblyComplex{T<:Real} = Union{T, Complex{T}}

f(::MyData{T, <:PossiblyComplex{T}}, ::Number) where {T<:Real} = "complex"
f(::MyData{T, T}, ::Real) where {T<:Real} = "real"

did a good job for me.

Although I wonder, if there is any more concise solution, not requiring me to repeat the type T when defining methods…

I see a couple of options:

First, you could use a Union field for a, which makes the behavior of the struct nicely obvious:

julia> struct MyData{T <: Real}
         a::Union{T, Complex{T}}
         b::T
       end

julia> MyData(1.0, 2.0)
MyData{Float64}(1.0, 2.0)

julia> MyData(1.0 + 2im, 2.0)
MyData{Float64}(1.0 + 2.0im, 2.0)

This doesn’t let you dispatch on MyData{<:Complex}, so it would require doing something like:

if isreal(my_data.a)
  ...

which might not be ideal, but could be fine.

Another option would be to use GitHub - vtjnash/ComputedFieldTypes.jl: Build types in Julia where some fields have computed types which kind of makes your original example just work:

julia> using ComputedFieldTypes

julia> @computed struct MyData{T<:Number}
               a::T
               b::real(T)
       end
fulltype (generic function with 2 methods)

julia> MyData{Float64}(1.0, 2.0)
MyData{Float64,Float64}(1.0, 2.0)

julia> MyData{ComplexF64}(1.0, 2.0)
MyData{Complex{Float64},Float64}(1.0 + 0.0im, 2.0)

ComputeFieldTypes.jl still creates a second type parameter for b, but it lets you avoid typing it out in most cases.

Thanks for the hints! I’d prefer dispatching, as I believe this to be faster and I know, that this happens in hot sections.

Do you happen to have any examples how dispatching looks with the ComputedFieldTypes.jl library? I didn’t find anything on their repo after a first glance.