[ANN] UnitTypes v1.0.0

This package provides physical units as Julia types.

julia> using UnitTypes

julia> x = Meter(3)
3m

julia> typeof(x)
Meter

julia> typeof(x) <: AbstractLength
true

julia> typeof(x) <: AbstractCapacitance
false

This allows you to easily write functions with arguments restricted to variables having certain types.

julia> function goFaster(a::T) where T<:AbstractAcceleration end

This leads to correctness and very clear error messages.

julia> goFaster(3u"m/s")
ERROR: MethodError: no method matching goFaster(::MeterPerSecond)

Closest candidates are:
  goFaster(::AbstractAcceleration)

Type hierarchy

UnitTypes introduces an abstract type hierarchy of:

AbstractMeasure
├─ AbstractAcceleration
│  └─ MeterPerSecond2
├─ AbstractAngle
│  ├─ Degree
│  └─ Radian
├─ AbstractArea
│  ├─ Acre
│  ├─ Meter2
│  ├─ SquareFoot
│  └─ SquareMile
├...and so on

See the docs for more.

Macros are used to introduce and create relationships around new types:

  • @makeBaseMeasure Length Meter "m" - introduces a new basic Measure like Meter for Length or Meter3 Volume, this should be rarely used!
  • @makeMeasure Meter(1000) = KiloMeter(1) "km" - derives a new measure (KiloMeter) from some an existing measure (Meter) with a conversion ratio (1000m = 1km)
  • @relateMeasures KiloGram*MeterPerSecond2=Newton - relates the product of types to another type, all types preexisting.

Comparison with other packages

Unitful

Unitful leverages parametric types to store units, giving flexibility at the cost of compile-time type uncertainty. It’s two major limitations are the avoidance of angular measures, as they are not first-class entities but rather ratios, and rather lengthy type unions that clutter outputs, especially on error:

julia> function goSlower(x<:Unitful.Acceleration) end
goSlower (generic function with 1 method)

julia> goSlower(1u"mm")
ERROR: MethodError: no method matching goSlower(::Quantity{Int64, 𝐋 , Unitful.FreeUnits{(mm,), 𝐋 , nothing}})

Closest candidates are:
  goSlower(::T) where T<:(Union{Quantity{T, 𝐋 𝐓^-2, U}, Level{L, S, Quantity{T, 𝐋 𝐓^-2, U}} where {L, S}} where {T, U})

As Unitful is the dominant unit package and has wide use and support, we provide a separate package ExchangeUnitful to enable interoperation with Unitful.

DynamicQuantities

DynamicQuantities is newer and faster than Unitful because it “defines a simple statically-typed Quantity type for storing physical units.” It does this by storing the exponents on the basic units, allowing any unit traceable to SI to be used. But this performant representation hurts readability, and while the unit representation may be able to be hidden behind overrides of show(), Julia is designed for types to be read and manipulated directly by users.

Enter UnitTypes

In the presence of Julia’s type-first UI, these two, good attempts feel misdirected and motivate this package’s literal typing of units. The limitation is that UnitTypes does not have a catch-all unit representation. Only units that have been defined by one of the macros may be represented, and complex units may need to have additional methods written to correctly convert between units.

Corrections, suggestions, contributions, and questions are welcome here or in an issue!

13 Likes

Trying out alternative unit designs is nice! Unitful.jl is great and covers almost all usecases, but it’s by no means perfect. I definitely have a set of gripes related to it :slight_smile:

Still, I wonder if

can potentially be solved with minor modifications to Unitful.jl design? Eg, simplify types or introduce shorter type aliases for common cases.

Regarding

Is it potentially possible in the future to support arbitrary units (or SI prefixes) without defining each of them manually? Or this is a fundamental limitation?

Btw,

is not universally the case: Unitful.jl is generally faster when the code is type-stable – that is, unless one has runtime-dynamic units.

3 Likes

Nice to see more exploration. I too wonder how much of the benefit of nicely named abstract types could be had by defining nicely named aliases – leaving more complex units (like x^4) ugly but working:

julia> abstract type MyAbstractMeasure{L,T,M,Etc} end

julia> const MyAbstractVolme = MyAbstractMeasure{3,0,0,0};

julia> struct MyMeter3 <: MyAbstractVolme; x::Float64; end

julia> ff(x::MyAbstractVolme) = "yes, it's a volume";

julia> ff(x)
ERROR: MethodError: no method matching ff(::Meter)

Closest candidates are:
  ff(::MyAbstractVolme)
   @ Main REPL[50]:1

Trying to define such aliases for Unitful doesn’t affect method errors, I’m not really sure why. (Edit, from your Unitful.Acceleration I learn that Unitful already has such aliases, but they also don’t show up.)

julia> using Unitful: Quantity, m, ft, 𝐋

julia> const UAbsVol{T} = Quantity{T, 𝐋^3}
Quantity{T, 𝐋³} where T

julia> gg(x::UAbsVol) = "yes it's a volume"

julia> gg(1ft^3)
"yes it's a volume"

julia> gg(1m)
ERROR: MethodError: no method matching gg(::Quantity{Int64, 𝐋, Unitful.FreeUnits{(m,), 𝐋, nothing}})

Closest candidates are:
  gg(::Quantity{T, 𝐋³} where T)
   @ Main REPL[21]:1

julia> hh(::Unitful.Volume) = "yes it's a volume";

julia> hh(1m)
ERROR: MethodError: no method matching hh(::Quantity{Int64, 𝐋, Unitful.FreeUnits{(m,), 𝐋, nothing}})

Closest candidates are:
  hh(::Union{Quantity{T, 𝐋³, U}, Level{L, S, Quantity{T, 𝐋³, U}} where {L, S}} where {T, U})
   @ Main REPL[31]:1
1 Like

I have not experimented too much with units in Julia, but like the concept and like the concept of SI prefixes. InspectDR.jl has dependency, NumericIO.jl, which outputs SI prefixes (among other things). It might be interesting to see if this would be useful in these applications.

1 Like

The inverse mapping of a unit label into a fuller type is the issue; “m” → Meter is hard when the package doesn’t know that “m” stands for Meter. But I’m certainly open to having a large library of predefined, common symbols.

SI prefixes could be expanded in u_str ( 1.2u"GB" => 1.2e9Bytes ) and assumed in measure2String() (1.2e9 Bytes => 1.2GB) but I feel this would quickly devolve into printf-like format strings.

There’s been plenty of discussion here and here but much of my point is that Unitful’s design is overly complicated for many use cases, compared to the straightforward approach here. That first thread mentions a few packages that tackle Unitful’s verbosity, but it’s still there underneath. And fixing the display verbosity doesn’t give you clear, simple types.

I’ve written a Unit library with a similar type hierachy to this. It just makes a lot of sense conceptually so I keep using it over other libraries.

You might want to check out how I defined composite units because it enables you to mix your approach and the Unitfull approach without any correctness issues. It’s just a question of picking which composite types you want to define a special type for. In my library Volt and Angle have special types defined while Speed doesn’t so Speed shows up in stack traces as Length/Time while Volt just shows up as V

3 Likes