Equality of unwrapped unionalls

Say you have the following struct

struct MyArr{T,N}
  arr::Array{T,N}
end

I was then wondering why the following equality does not hold:

unwrap_arr_field = fieldtypes(Base.unwrap_unionall(MyArr))[1]
unwrap_arr_manual = Base.unwrap_unionall(Array{T,N} where {T,N})

@assert (unwrap_arr_field == unwrap_arr_manual) == false
@assert (unwrap_arr_field <: unwrap_arr_manual) == false
@assert (unwrap_arr_manual <: unwrap_arr_field) == false

But

unwrap_arr_field.hash == unwrap_arr_manual.hash

Expectedly we have

@assert fieldtypes(MyArr)[1] == Array{T,N} where {T,N}

So there is something happening during the unwrapping of the UnionAll.

I’ve the suspicion that this is due to typevars not being equal although being identical

@assert (TypeVar(:T) == TypeVar(:T)) == false

but I don’t really know why this is or if this is indeed the case.

What am I missing in my mental model of julia types? Why does this happen? What happens to the unwrapped unionall type that breaks the equality?

In Julia and programming languages in general, identity means the “same” instance that cannot be distinguished by any program; this is different than the colloquial sense of similarity. For example, we can distinguish “identical” twins by giving them differently colored hats. For a coding example, twin arrays [1] and [1] can be distinguished by mutating them to different values. In fact, separate instantiations of a mutable type always result in different instances by definition.

Back to your case, the T are not identical (===) despite having the same internal values because TypeVar is a mutable type, and equality (==) falls back to === by default:

julia> unwrap_arr_manual.parameters[1] == unwrap_arr_field.parameters[1]
false

julia> unwrap_arr_manual.parameters[1] === unwrap_arr_field.parameters[1]
false

julia> unwrap_arr_manual.parameters[1], unwrap_arr_field.parameters[1]
(T, T)

julia> typeof.((unwrap_arr_manual.parameters[1], unwrap_arr_field.parameters[1]))
(TypeVar, TypeVar)

julia> ismutabletype(TypeVar)
true

Note that all the fields of TypeVar are const so we can’t reassign them; despite the effective inability to mutate them to different values, they are still distinguishable by memory address, and that is what === currently checks.

Ok, thank you for the clarification. I had mistakenly assumed that TypeVars were immutable. The const mutable implementation seems a very explicit way of defining them, which leads me to the followup question of: why do typevars need to be separated even at the instance level even when indicating the same parameter and bounds?

It’s easier to flip the question, when do you need the same TypeVar instance? When the same type parameter is shared by several places, like T in the line struct MyArr{T,N} and the line arr:Array{T,N} of the definition:

julia> Base.unwrap_unionall(MyArr).parameters[1] === unwrap_arr_field.parameters[1]
true

TypeVar identity is how Julia ensures both places get the same T value you specify per concrete type. Flipping back to your question, we thus need different instances for different type parameters that happen to share a name and bounds but can be specified with different values. That might have been doable with an immutable type if there was an additional field for identity, but everything has an address, might as well use it.

Source: More about types · The Julia Language

1 Like