Problem with Complex{Rationals}

Hello,
I have found something which makes it difficult to work with the field Q(i)

julia> Complex{Rational{Int}}<:Complex{Real}
false

Is it possible to make julia understand that this should be true?

1 Like

You are hitting the covariance / contravariance distinction, see https://docs.julialang.org/en/stable/manual/types/#Parametric-Composite-Types-1.

You have for example:

julia> Complex{Rational{Int}} <: Complex{<:Real}
true

where Complex{<:Real} = Complex{T} where {T <: Real}

2 Likes

That’s just type covariance. Read https://docs.julialang.org/en/stable/manual/types/#Parametric-Composite-Types-1 . But basically if your issue was dispatch then you’re do

function f(x::Complex{T}) where T<:Real

(oh, jinx! :smile:)

4 Likes

Thank you! It was not for dispatching but to make a test. It is a bit mind-boggling that

julia> Complex{Rational{Int}} <: Complex{<:Real}
true

but

julia> Complex{Rational{Int}} <: Complex{Real}
false

It will take me some time to be completely confortable with julia’s type system

1 Like

For example:

A vector of Complex{Real} is able to hold values with different types:

julia> Complex{Real}[Complex(1, 2), Complex(1.0, 2.0)]
2-element Array{Complex{Real},1}:
   1+2im  
 1.0+2.0im

A vector of Complex{<:Real} can hold values of Complex{T} where T <: Real but all values must be of the same type. In a sense, you are free to choose T <: Real but after you have chosen it, you are stuck with it.

5 Likes

Ok, it makes sense when you think of collections. I guess this is how you have to think about types.

You should also understand that this distinction is absolutely critical for performance. It’s not just an arbitrary pedantic choice. Think about how instances of the types are actually stored and processed on the computer.

A Complex{Rational{Int}} for a+bi can be (and is) stored as four consecutive Int (64-bit integer) values: (anum,aden,bnum,bden). For operations on such quantities, since the compiler knows everything’s type and how it is stored, it can load the numerators and denominators directly into integer registers and perform arithmetic immediately with corresponding machine instructions. An array of n such values is stored as an array of 4n 64-bit integers consecutively in memory.

Conversely, if you have a Complex{Real} value, then the real and imaginary parts can be of any Real type. That means that they must each be a “box” that has a type tag (to indicate what Real type it is) followed by the actual value. Since these values may be of different sizes depending on the type, to store a Complex{Real} instance in a fixed amount of memory it must store pointers to the boxes. So, to work with a Complex{Real} value, the processor must fetch the pointers, then use the pointers to fetch the type tags, then dispatch to code that does + (for example) on the actual values for those types. Huge overhead! And an array of Complex{Real} objects must be an array of pairs of pointers to boxes — looping over the array to do some operation, since every element can be a different type you need to do the pointer-chasing and dynamic dispatch for every element.

If you have a compiled function that operates on a Complex{Real} or an array of Complex{Real}, therefore, it is completely different from code that operates on Complex{Rational{Int}}. You can’t use either one for the other. (If A is a subtype of B, it essentially means “compiled code for B can be used on instances of A” — this is the purpose of the subtype relation, to decide what code is applicable to what types.)

This is why Julia’s parametric types have the “invariant” semantics that they do. Once you get used to it, it’s quite easy to work with, and allows you to have generic types like Complex that are still fast and efficient for concrete instances.

27 Likes

Is there speed advantage of the signature
f(x::T) where {T <: SomeAbstractType} = ...
over the
f(x::SomeAbstractType) = ... ?

No. Indicating types in method signatures is generally speaking irrelevant for performance (even if you just define f(x) =... it will have the same speed) . It’s more like defining a user interface in the sense of only allowing certain types to be passed to the function. However, even this is identical on your example.

3 Likes