# 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! )

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 an `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