`Unitful`: define units in module and use them outside

I’m reading the documentation (linked at the bottom of this message) about how to define new units and have come up with a module like this

module MyUnits
using Unitful

Unitful.register(@__MODULE__) # What does this do?

@unit pt "pt" pt (1//72)*u"inch" false

const mm = u"mm"
const cm = u"cm"
const pt = u"pt"
end

and use this module in the main program:

push!(LOAD_PATH,pwd())
using MyUnits: mm, pt

println(1mm/1pt) # -> 1.0 mm pt⁻¹
println(1mm/1pt + 0.0) # -> ERROR: LoadError: KeyError: key :pt not found

What is going wrong?

Seems to work fine for me in REPL (Julia v1.9.4, Unitful v1.19.0).

julia> module MyUnits
       using Unitful

       Unitful.register(@__MODULE__) # What does this do?

       @unit pt "pt" pt (1//72)*u"inch" false

       const mm = u"mm"
       const cm = u"cm"
       const pt = u"pt"
   end
Main.MyUnits

julia> using .MyUnits: mm, pt

julia> println(1mm/1pt)
1.0 mm pt^-1

julia> println(1mm/1pt + 0.0)
2.8346456692913384

Try these same lines in REPL like I did above. If it still fails, then maybe there’s something different about the environments or package versions that’s responsible for the issue. If it works for you in REPL, then the issue must have something to do with either how the module file is written/defined or something to do with how it’s loaded.

1 Like

It does work! But it fails as below

julia> include("mainprogram.jl")
[ Info: Precompiling MyUnits [top-level]
1.0 mm pt⁻¹
ERROR: LoadError: KeyError: key :pt not found
Stacktrace:
  [1] getindex(h::Dict{Symbol, Tuple{Float64, Rational{Int64}}}, key::Symbol)
    @ Base ./dict.jl:484
  [2] basefactor
    @ ~/.julia/packages/Unitful/R4J37/src/units.jl:262 [inlined]
  [3] map
    @ ./tuple.jl:274 [inlined]
  [4] basefactor(#unused#::Unitful.FreeUnits{(mm, pt⁻¹), NoDims, nothing})
    @ Unitful ~/.julia/packages/Unitful/R4J37/src/units.jl:265
  [5] #s103#141
    @ ~/.julia/packages/Unitful/R4J37/src/conversion.jl:13 [inlined]
  [6] var"#s103#141"(::Any, s::Any, t::Any)
    @ Unitful ./none:0
  [7] (::Core.GeneratedFunctionStub)(::Any, ::Vararg{Any})
    @ Core ./boot.jl:602
  [8] promote_rule
    @ ~/.julia/packages/Unitful/R4J37/src/promotion.jl:99 [inlined]
  [9] promote_type
    @ ./promotion.jl:307 [inlined]
 [10] _promote
    @ ./promotion.jl:357 [inlined]
 [11] promote
    @ ./promotion.jl:381 [inlined]
 [12] +(x::Unitful.Quantity{Float64, NoDims, Unitful.FreeUnits{(mm, pt⁻¹), NoDims, nothing}}, y::Float64)
    @ Base ./promotion.jl:410
 [13] top-level scope
    @ . . . mainprogram.jl:5
 [14] include(fname::String)
    @ Base.MainInclude ./client.jl:478
 [15] top-level scope
    @ REPL[1]:1
in expression starting at . . . /mainprogram.jl:5

julia>

The main program is

push!(LOAD_PATH,pwd())
using MyUnits: mm, pt

println(1mm/1pt) # -> 1.0 mm pt⁻¹
println(1mm/1pt + 0.0) # -> ERROR: LoadError: KeyError: key :pt not found

MyUnits.jl and the main program are in the same directory and I run the REPL in the same directory.

Julia Version 1.9.4 on macOS 14.2.1 .

I just tried putting the module contents above into a MyUnits.jl file, include’ing that file, and then running the two println tests: still no issue.

Seems like something about using the module from the LOAD_PATH is causing the issue.

(@v1.9) pkg> activate --temp
  Activating new project at `C:\Users\x\AppData\Local\Temp\jl_Nmrvor`

julia> cd("C:\\Users\\x\\AppData\\Local\\Temp\\jl_Nmrvor")

julia> push!(LOAD_PATH,pwd())
4-element Vector{String}:
 "@"
 "@v#.#"
 "@stdlib"
 "C:\\Users\\x\\AppData\\Local\\Temp\\jl_Nmrvor"

julia> using MyUnits: mm, pt
[ Info: Precompiling MyUnits [top-level]

julia> println(1mm/1pt)
1.0 mm pt^-1

julia> println(1mm/1pt + 0.0)
ERROR: KeyError: key :pt not found
Stacktrace:
  [1] getindex(h::Dict{Symbol, Tuple{Float64, Rational{Int64}}}, key::Symbol)
    @ Base .\dict.jl:484
  [2] basefactor
    @ C:\Users\x\.julia\packages\Unitful\R4J37\src\units.jl:262 [inlined]
  [3] map
    @ .\tuple.jl:274 [inlined]
  [4] basefactor(#unused#::Unitful.FreeUnits{(mm, pt^-1), NoDims, nothing})
    @ Unitful C:\Users\x\.julia\packages\Unitful\R4J37\src\units.jl:265
  [5] #s103#141
    @ C:\Users\x\.julia\packages\Unitful\R4J37\src\conversion.jl:13 [inlined]
  [6] var"#s103#141"(::Any, s::Any, t::Any)
    @ Unitful .\none:0
  [7] (::Core.GeneratedFunctionStub)(::Any, ::Vararg{Any})
    @ Core .\boot.jl:602
  [8] promote_rule
    @ C:\Users\x\.julia\packages\Unitful\R4J37\src\promotion.jl:99 [inlined]
  [9] promote_type
    @ .\promotion.jl:307 [inlined]
 [10] _promote
    @ .\promotion.jl:357 [inlined]
 [11] promote
    @ .\promotion.jl:381 [inlined]
 [12] +(x::Unitful.Quantity{Float64, NoDims, Unitful.FreeUnits{(mm, pt^-1), NoDims, nothing}}, y::Float64)
    @ Base .\promotion.jl:410
 [13] top-level scope
    @ REPL[7]:1

I don’t know much about the mechanics beneath and don’t see any obvious indications in the Module docs that would tell me to expect a difference in behavior between these loading styles. I don’t know whether this would be a bug in Unitful, if it’s the intended behavior, or some nuance in Julia’s package loading logic that I don’t understand.

1 Like

The issue seems to be the value of @__MODULE__ which is Main.MyUnits in the successful load paths and just MyUnits in the erroring paths.
Still investigating, but maybe someone else already gets the problem.

I found a newer example here in the Unitful docstring that makes me think it’s something to do with how this is implemented in Unitful.

Based on the Module docs here, a function __init__() in a module is executed at runtime vs compile time… I’m not sure I quite understand this distinction because working this way I get the error at precompile time.

I tried defining a MyUnits.jl file, per the Unitful.register docstring linked above:

module MyUnits
	using Unitful

	@unit pt "pt" pt (1//72)*u"inch" false

	const mm = u"mm"
	const cm = u"cm"
	const pt = u"pt"
	
	function __init__()
		Unitful.register(MyUnits)
	end
end

Now I get the following error, instead.

julia> using MyUnits: mm, pt
[ Info: Precompiling MyUnits [top-level]
ERROR: LoadError: ArgumentError: Symbol pt could not be found in unit modules Module[Unitful]
Stacktrace:
 [1] lookup_units(unitmods::Vector{Module}, sym::Symbol)
   @ Unitful C:\Users\x\.julia\packages\Unitful\R4J37\src\user.jl:707
 [2] var"@u_str"(__source__::LineNumberNode, __module__::Module, unit::Any)
   @ Unitful C:\Users\x\.julia\packages\Unitful\R4J37\src\user.jl:639
 [3] include
   @ .\Base.jl:457 [inlined]
 [4] include_package_for_output(pkg::Base.PkgId, input::String, depot_path::Vector{String}, dl_load_path::Vector{String}, load_path::Vector{String}, concrete_deps::Vector{Pair{Base.PkgId, UInt128}}, source::Nothing)
   @ Base .\loading.jl:2049
 [5] top-level scope
   @ stdin:3
in expression starting at C:\Users\x\AppData\Local\Temp\jl_znDJGl\MyUnits.jl:8
in expression starting at C:\Users\x\AppData\Local\Temp\jl_znDJGl\MyUnits.jl:1
in expression starting at stdin:3
ERROR: Failed to precompile MyUnits [top-level] to "C:\\Users\\x\\.julia\\compiled\\v1.9\\jl_9F38.tmp".
Stacktrace:
 [1] error(s::String)
   @ Base .\error.jl:35
 [2] compilecache(pkg::Base.PkgId, path::String, internal_stderr::IO, internal_stdout::IO, keep_loaded_modules::Bool)
   @ Base .\loading.jl:2294
 [3] compilecache
   @ .\loading.jl:2167 [inlined]
 [4] _require(pkg::Base.PkgId, env::String)
   @ Base .\loading.jl:1805
 [5] _require_prelocked(uuidkey::Base.PkgId, env::String)
   @ Base .\loading.jl:1660
 [6] macro expansion
   @ .\loading.jl:1648 [inlined]
 [7] macro expansion
   @ .\lock.jl:267 [inlined]
 [8] require(into::Module, mod::Symbol)
   @ Base .\loading.jl:1611

I’m a moderate user of Unitful.jl and have made significant use of it in my own packages, but I’ve always been a little puzzled by the unit definition/registration system. Now, TIL __init__ module functions are a thing. The more you know, I guess.

I think you want to base your units module on:

There are a few things to note:

  • @unit creates a local binding to pt, so const pt = u"pt" is not necessary.
  • To have pt, mm, cm available in the enclosing module you want to export them.

Something like:

module MyUnits
    using Unitful
    export pt, mm, cm
    @unit pt "pt" pt (1//72)*u"inch" false

    const mm = u"mm"
    const cm = u"cm"

    Unitful.register(@__MODULE__)
    function __init__()
        return Unitful.register(@__MODULE__)
    end               
end
1 Like

Thank you!!! That’s it. At the bottom of this message, I show a complete example that works, using this method. [Could somebody add this to the documentation? If I were familiar with github, I’d create a PR, but each time I try to start github, I realize the steep initial learning curve and have to give it up. I’m not a developer and I hardly has a motivation to master github except for this situation.]

Before showing the code,

I deliberately don’t do so for this particular case. I don’t want to spill little symbols like mm into my code without explicitly declaring them. If you included export pt, mm, cm in your module, you get this result:

using MyUnits # -> implicitly exports pt, mm, cm
nn = 92
mm = 9 # I wanted to use this symbol!
. . .

This problem is likely to get bigger in the future as I keep adding custom units and convenience symbols to MyUnits.jl. So I choose between

using MyUnits: pt  # when I use pt often

and

using MyUnits
MU = MyUnits
. . .
a = 3(MU.pt) # -> 3 pt

The module file that works is

module MyUnits
using Unitful

@unit pt "pt" pt (1//72)*u"inch" false

const mm = u"mm"
const cm = u"cm"
#const pt = u"pt" # not necessary, because @unit does that.

Unitful.register(@__MODULE__)
function __init__()
  return Unitful.register(@__MODULE__)
end

end

and the main program that works is

push!(LOAD_PATH,pwd())
using MyUnits: mm, pt
println(1mm/1pt) # -> 1.0 mm pt⁻¹
println(1mm/1pt + 0.0) # -> 2.8346456692913384

This is explained in the documentation here, but indeed providing it explicitly as something like below would be beneficial! Also maybe providing a link to this page from the “Defining new units” section.

module YourModule
    # define new units here 

    # Unitful.register(YourModule)  # uncomment if you use u-strings with newly defined units
    function __init__()
        return Unitful.register(YourModule)
    end
end
1 Like

Would it be even possible to provide a macro?

module YourModule
# define new units here
@register_all
end

I’m not so familiar with macros as to be able to tell whether this is even possible or not.

there is such a link there… my bad.

I don’t know that using a macro for defining the init function (if even possible) is a good idea. since it would hide this function. I think that requiring packages to include the register line in their init function is not a big deal.

Also, I think you might be OK without the call to register in the main body of the Module since you never use u-strings with the newly defined units (e.g. you never say u"pt").

The problem is that the current Unitful package requires such obscure knowledge as that, on the part of the “mere” user. To understand what you have just said, I would have to read the documentation and understand what __init__ does and what register does.

If you read through this thread, you realize some other users also failed to fully understand how register works. The current documentation around __init__ tries to give the knowledge, but what the “mere” user needs is a simple piece of code that works without having to know how Unitful is internally implemented.

I don’t know that using a macro for defining the init function (if even possible) is a good idea. since it would hide this function. 

If the user wants to define her/his own init function, s/he must be more knowledgeable than the average user. So, this might be fine for such a user:

@register_all begin
   # Write the contents of __init__
end

whereas for a user who doesn’t need to think about __init__

@register_all

Of course, I’m just imagining things without having much knowledge about macros.

I am not a Unitful maintainer; if you feel strongly about this I recommend creating a GitHub issue in their repo. But here are my two cents:

What is “obscure” and what is a “mere user”? These depend greatly on your own learning path. I would say that if you are trying to create a module you are a step beyond the most basic user (one that uses modules but not creates new ones) and you have the extra burden of learning how modules work, like knowing that modules can have an __init__ function. Both the documentation on modules, and using this forum are good ways to learn it, as you have done. You now know this and can use this knowledge whenever you create new modules. The init function is something related to modules, and I don’t think that Unitful documentation is the right place to explain it, or that Unitufl should provide ways around it. They should explain why you need to use register in your init. If you think this is not explained clearly you should create an issue and/or pull request on their GitHub site. Documentation can always be improved! But creating a macro to avoid creating the init function is not the right move IMO. Note that the only “extra” when creating a module that defines new units is a single line in your init function! Once you know this and understand why it is required it should not be any more difficult than using a macro and is much clearer.

1 Like

I pretty much concur with this. The issue for me was mostly in discovery of the documentation for what code snippet would be required, not that code itself. I’ve got a couple of focused Julia packages and somehow haven’t yet had a need to implement an __init__ function for one, but it was easy enough to find them listed in the Module docs. If anything, they could’ve also just linked to that part of the docs so that it’s obvious that’s a function with a special purpose.

1 Like