Julia's type system : what am I missing?

Consider this MWE:

using Unitful

# 1. Get the type of a quantity constructed from the Unitful macro
t1 = typeof(1u"m")

# 2. Manually construct a type with the same structure
t2 = Unitful.Quantity{Int64, Unitful.๐‹, Unitful.FreeUnits{(Unitful.m,), Unitful.๐‹, nothing}}

# 3. Compare
println("t1 == t2: ", t1 == t2)      # false โ€“ structurally different
println("t1 === t2: ", t1 === t2)    # false โ€“ different type objects in memory

This has come up before, though I canโ€™t find the other thread at the moment. I think the type isnโ€™t being printed faithfully. You can do:

t3 = Quantity{Int64, dimension(u"m"), typeof(u"m")}

@show t1 == t3 # true
@show t1 === t3 # also true
3 Likes

Thanks for the response; this is a very subtle issue that took a long time to track down. Since dispatch depends on the faithful comparison of types, what is a good strategy to prevent this problem?

Hereโ€™s a related thread: How To Properly Use Unitful - #12 by ajkeller34

2 Likes

OMG :slight_smile:

Wow, thatโ€™s very cursed. Itโ€™s the difference between:

julia> Unitful.FreeUnits{(Unitful.Unit{:Meter, Unitful.๐‹}(0, 1//1),), Unitful.๐‹, nothing}()
m

julia> Unitful.Unit{:Meter, Unitful.๐‹}(0, 1//1)
m

Both of those things print as m, but only one of them is the same as Unitful.m.

julia> typeof(Unitful.m)
Unitful.FreeUnits{(m,), ๐‹, nothing}
1 Like

In the end I wanted to do something like this:

const DeviceUnits{T} = Union{
    Quantity{T, dimension(u"m"), typeof(u"m")},
    Quantity{T, dimension(u"cm"), typeof(u"cm")}} where T <: Number

and there was no easy way to construct the types. This works but the prior construction which used

Quantity{Int64, Unitful.๐‹, Unitful.FreeUnits{(Unitful.m,), Unitful.๐‹, nothing}}

did not.

I often find myself fighting the Julia type system.

Iโ€™m not adept enough to understand the mechanism that enables this.

Itโ€™s not about Juliaโ€™s type system; itโ€™s that Unitful is choosing to print two subtly different values as the same thing. And then it also defines a variable with the same name as what prints out.

It just becomes particularly confusing when you put these values into a type parameter. Hereโ€™s whatโ€™s effectively happening:

By default, Julia prints different values differently:

julia> struct A end

julia> struct B end

julia> A()
A()

julia> B()
B()

But you can override that:

julia> Base.show(io::IO, ::A) = print(io, "something")

julia> Base.show(io::IO, ::B) = print(io, "something")

julia> A()
something

julia> B()
something

And then it becomes even more confusing if that value ends up as a parameter of a typeโ€ฆ or if you have another variable named something.

julia> t1 = Val{A()}
Val{something}

julia> t2 = Val{B()}
Val{something}

julia> t1 == t2
false
8 Likes

aha, this makes dispatch with Unitful types particularly painful. I read in the docs that they recommend avoiding it. Thanks for the response!

I donโ€™t know if this was missed in the discussion, but I frequently use dispatch on the supertypes like Unitful.Length based on the SI dimensions.

foo(x::Unitful.Length) = "Is a Unit of Length"
foo(x::Unitful.AbstractQuantity) = "Is NOT a Unit of Length"

julia> foo(1u"m")
"Is a Unit of Length"

julia> foo(1.0u"ft")
"Is a Unit of Length"

julia> foo(1u"kg")
"Is NOT a Unit of Length"

If not already defined, the @derived_dimension macro can be used for a different combination of SI dimensions.

4 Likes

Adding to @Daniel_Berge 's answer, you can easily dispatch on more specific aliases:

const Len{T} = Quantity{T,u"๐‹"}
const Met{T} = Quantity{T,u"๐‹",typeof(m)}
const Deg{T} = Quantity{T,NoDims,typeof(ยฐ)}
const Rad{T} = Quantity{T,NoDims,typeof(rad)}

You donโ€™t need to construct concrete Unitful.jl types for dispatch. And you can let Julia figure out the exact type in a field using a type parameter:

struct Foo{L<:Met}
  len::L
end
5 Likes