OptionalUnits

Having the appropiate physical units attached to your variables is a good way to protect yourself from accidental miscalculations and keeps you from wondering if that float is supposed to be in m or mm. However when using Unitful I reached a point, where I either had to use units in all of my code (which is not something that I want to be forced to do) or I found myself writing wrapper functions that added an implicit unit to the raw number and passed it to the function using Unitful. To automate this process I created this package.

This package defines the @optionalunits macro that can be attached to a function or struct definition to automatically define a function or constructor that can either use a Unitful.Quantity of the right dimension or use a raw number with an implicit unit allowing the user of the function to choose either the error detection mechanism of Unitfuls explicit units or the simplicity of raw numbers with implicit units.

Usage

Functions

Warning

Currently the @optionalunits macro only works with the function f(x) end syntax, the shorthand form f(x)= is not yet supported!

Applied to a function definition the @optionalunits macro can be used like this:

@optionalunits function addOneMeter(x::Unitful.Length→u"m")
    return x+1u"m"
end

Behind every type parameter that is a Unitful.Dimension a default unit can be annotated with → (\rightarrow[TAB]]). The macro changes the function definition to the following:

function addOneMeter(x::Union{Unitful.Length,Real})
    if Unitful.dimension(x) == NoDims
        @warn "Used default unit m for" x
        x *= u"m"
    end
        
    return x+1u"m"
end

The macro also works with array-like types of uniform dimension

@optionalunits function addOneMeter(x::Vector{Unitful.Length→u"m"})
    return x.+1u"m"
end

the new function definition looks slightly different:

function addOneMeter(x::Union{Vector{<:Unitful.Length},Vector{<:Real}})
    dims = Unitful.dimension(x)
    @assert all(dims .== [first(dims)]) "The array-like type x has mixed dimensions which is not supported by @optionalunits"
    if first(dims) == NoDims
        @warn "Used default unit m for array-like type" x
        x *= u"m"
    end
        
    return x.+1u"m"
end

This definition allows the function to be called with or without units:

julia> addOneMeter(1u"m")
2 m

julia> addOneMeter(1.0u"mm")
1.001 m

julia> addOneMeter(1.0)
┌ Warning: Used default unit m for
│   x = 1.0
└ @ Main REPL[18]:3
2.0 m

julia> addOneMeter(1.0u"m/s")
ERROR: MethodError: no method matching addOneMeter(::Quantity{Float64, 𝐋  𝐓 ^-1, Unitful.FreeUnits{(m, s^-1), 𝐋  𝐓 ^-1, nothing}})

Of course multiple annotated and unannotated arguments, the combination of these two versions, and the use of optional arguments works.

Structs

To use units in a struct Unitful recommends using a concrete type for every field, i.e. a Unitful.Quantity with a fixed datatype, dimension and unit. Therefore, no extra annotation of default units is needed to use the @optionalunits macro. When applied on a struct definition the macro redefines the default outer constructor (with all Any parameters) and adds the fallback to use the default units:

@optionalunits struct Point
    x::typeof(1.0u"m")
    y::typeof(1.0u"m")
end

The struct definition itself is not changed, but the following outer constructor is defined:

function Point(x,y)
    if dimension(x) == NoDims
        @warn "Used default unit m for " x
        x *= u"m"
    end
    x = Base.convert(Core.fieldtype(Point, 1), x)

    if dimension(y) == NoDims
        @warn "Used default unit m for " y
        y *= u"m"
    end
    y = Base.convert(Core.fieldtype(Point, 2), y)

    return Point(x,y)
end

The same principle applies to fields of an array-like type.

The code can be found on GitHub.

Exports

OptionalUnits.@optionalunitsMacro

Can be applied to function and struct definitions that use Unitful types to add default units, when the parameters that are passed to the function do not have a unit attached.

In a function definition the type parameter e.g. Unitful.Length is appended with the default unit (e.g. x::Unitful.Length→u"mm")

In a struct definition nothing additional has to be added, the field types with Unitful.Quantity will be detected

Examples

julia> using OptionalUnits, Unitful

julia>  @optionalunits function addOneMeter(x::Unitful.Length→u"m")
            return x+1u"m"
        end

julia>  @optionalunits function addOneMeter(x::Vector{Unitful.Length→u"m"})
            return x.+1u"m"
        end

julia> @optionalunits struct Point
            x::typeof(1.0u"m")
            y::typeof(1.0u"m")
        end

julia>  Point(1,2)
source