Unitful.jl | possibly mislabelled AbstractTypes, rare sametype() error | new user observations

Hello everyone;

I have been getting the hang of the very nice Unitful.jl package recently. I really like it, and would like to take the chance to thank those in the community who develop(ed) and maintain(ed) this package!

I thought it might be worth sharing a two observations that came up while I’ve been learning (so far); perhaps it will help someone else in the future.

  1. Unitful.jl: document the difference between AbstractType and @derived_dimension
  2. sametype() error when comparing Unitful.jl values

Please note; neither observation means I cannot use Unitful.jl happily or productively. Rather I hope just that these things are just nice-to-know and/or good-to-have-on-record.

Thanks for reading and have a nice day!
~~

P.S. I’m an engineer by training, so please consider me essentially a computer science novice. I’m happy to help try to ‘improve’ things in the package, (e.g. documentation?) if I am able to be helpful in this way, but I am not currently sure how I would even start with such a thing or who I should speak with.



Unitful.jl: document the difference between AbstractType and @derived_dimension

In the Unitful.jl documentation section called ‘Dimensions in a type definition’. To me it is saying that Unitful.Length and Unitful.Mass are abstract types:

It may be tempting to specify the dimensions of a quantity in a type definition, e.g.

struct Person
    height::Unitful.Length
    mass::Unitful.Mass
end

However, these are abstract types. If performance is important, it may be better just to pick a concrete Quantity type.

As such, this result seems to be contradictory (to me):

julia> Base.isabstracttype(Unitful.Length)
false

julia> Base.isabstracttype(Unitful.Mass)
false

Even though these are behaving (mostly) like I would expect from AbstractTypes:

julia> typeof(1u"m") <: Unitful.Length
true

julia> typeof(u"m") <: Unitful.Length
false

From what I can see within Unitful.jl’s source code, specifically within pkgdefaults.jl’s code, these are @derived_dimension not abstract types.

There are four abstract types defined (which I could find within Types.jl’s code), which are recognised by Base.isabstracttype():

  • Unitful.Unitlike, Unitful.Units, Unitful.AbstractQuantity and Unitful.LogScaled

Questions/Suggestions

Perhaps one of the following suggestions could be considered for future updates to the Unitful.jl package?

  1. Unitful.jl’s documentation could be updated to clarify that they behave like abstract types, but will not return true when tested using Base.isabstracttype?
  2. Unitful.jl’s code could updated such that the ‘abstract types’ which are @derived_dimension such as Unitful.Length and Unitful.Mass so that they return true when tested using Base.isabstracttype?


sametype() error when comparing Unitful.jl values

The minimum working example included below reveals an error when passing Unitful.jl values, for example stored in a Vector into another function.

The error is ultimately thrown by the function sametype_error() (line 381 within Promotion.jl within Module Base) and I ask you kindly to run the MWE below to see the full stack trace for the error.

Details for reproducing the error

The error seems to (me) to appear only when:

  1. A vector of values with Unitful units, which will be passed to another function, is created with this syntax: Quantity{Number}[1*u"m"] where Number is any numerical AbstractType except for AbstractFloat, which seems to work fine.

  2. The function to which the values are passed includes a conditional comparison (e.g. ) involving the value passed to it.

Note that if the vector is created with an improved syntax such as simply [1*u"m"] then this error is no longer revealed.

I mention it here primarily in case:

  1. anyone else has seen this error and is wondering how to avoid it; and
  2. in case it has consequences which are beyond my understanding for the Unitful.jl package as a whole.
# Minimum Working Example 'MWE' - should run in REPL directly
using Unitful

# Functions to reveal error and test different numerical types
function unitful_double(Q̇::Unitful.VolumeFlow) :: Unitful.VolumeFlow
    return 2Q̇
end

function unitful_conditional_double(Q̇::Unitful.VolumeFlow) :: Unitful.VolumeFlow
    Q̇ ≤ 10u"L/minute" && return 2Q̇
end

function tests(value)
    println("The type of the input for this test is: \n",
            "   - $(typeof(value))\n",
    )

    try
        unitful_double(value)
        println("✔ Success: 'unitful_double()' worked for input with type:\n",
                "   - $(typeof(value))\n",
                )
    catch
        @warn string("❌ Failure: error thrown by 'unitful_double()' with input type:\n",
                     "  - $(typeof(value))\n",
                     )
    end

    try
        unitful_conditional_double(value)
        println("✔ Success: 'unitful_conditional_double()' worked for input with type:\n",
        "   - $(typeof(value))\n",
        )
    catch
        @warn string("❌ Failure: error thrown by 'unitful_conditional_double()' with input type:\n",
                     "  - $(typeof(value))\n",
                     )
    end
end

### Test different numerical types, both non-abstract and abstract ###
## Non-abstract numerical types ##
# Float64 #
isabstracttype(Float64)
val_from_vector_FLOAT64 = first(Quantity{Float64}[1*u"L/minute"])
tests(val_from_vector_FLOAT64)

# Int64 #
isabstracttype(Int64)
val_from_vector_INT64 = first(Quantity{Int64}[1*u"L/minute"])
tests(val_from_vector_INT64)

## Abstract numerical types ##

# AbstractFloat #
isabstracttype(AbstractFloat)
val_from_vector_ABSTRACTFLOAT = first(Quantity{AbstractFloat}[1*u"L/minute"])
tests(val_from_vector_ABSTRACTFLOAT)

# Integer #
isabstracttype(Integer)
val_from_vector_INTEGER = first(Quantity{Integer}[1*u"L/minute"])
tests(val_from_vector_INTEGER)

# Real #
isabstracttype(Real)
val_from_vector_REAL = first(Quantity{Real}[1*u"L/minute"])
tests(val_from_vector_REAL)

# Number #
isabstracttype(Number)
val_from_vector_NUMBER = first(Quantity{Number}[1*u"L/minute"])
tests(val_from_vector_NUMBER)

## Print full error to REPL
unitful_conditional_double(val_from_vector_NUMBER)


Julia version information:

julia> versioninfo()
Julia Version 1.8.0
Commit 5544a0fab76 (2022-08-17 13:38 UTC)
Platform Info:
  OS: macOS (x86_64-apple-darwin21.4.0)
  CPU: 4 × Intel(R) Core(TM) i5-5250U CPU @ 1.60GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-13.0.1 (ORCJIT, broadwell)
  Threads: 1 on 2 virtual cores
Environment:
  JULIA_EDITOR = code
  JULIA_NUM_THREADS = 

Unitful.jl version information:

(@v1.8) pkg> st Unitful
Status `~/.julia/environments/v1.8/Project.toml`
  [1986cc42] Unitful v1.11.0


The docs here are using “abstract type” in the common meaning of “a type that is not instantiable” i.e. something that’s not a concrete type:

julia> isconcretetype(Unitful.Length)
false

isabstracttype is very strict in what it checks for, as it says in its docs:

help?> isabstracttype
search: isabstracttype

  isabstracttype(T)

  Determine whether type T was declared as an abstract type (i.e. using the abstract keyword).

i.e. it only returns true for types that were declared specifically using the keyword abstract. Unitful.Length and others you tried happen to be Union types, so isabstracttype returns false for them. (Note that the manual section I’ve linked to also uses “abstract type” in the general meaning of it, saying: “A type union is a special abstract type”.)

When it comes to performance, however, what we care about is whether the type is concrete or not, as that has implications for how well the compiler can perform in generating fast code.

This usage is common in many parts of Julia, but still, it’s better to be clear and avoid confusion, especially in packages that are commonly used by non-developers. It seems best to just avoid using the phrase “abstract type” here, and instead change it to:

However, these are not concrete types. If performance is important, it may be better just to pick a concrete Quantity type.

This would prevent the confusion, while also avoiding a digression into what abstract types are and the behaviour of Base.isabstracttype.

1 Like

i.e. it only returns true for types that were declared specifically using the keyword abstract. Unitful.Length and others you tried happen to be Union types, so isabstracttype returns false for them. (Note that the manual section I’ve linked to also uses “abstract type” in the general meaning of it, saying: “A type union is a special abstract type”.)

  • Thank you very much for your help!
    → Going forward I will remember that a Union{} is not the same thing as an AbstractType.