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?

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