I’ve been pretty busy the last couple of days, so I’m only just getting back to this now.
Long story short, for a package that puts a lot of emphasis on the performance of dynamic dimension operations, I also think the current design is probably the right one: users define a new unit registry and carry over the existing seven dimensions from FlexUnits.
What I would suggest is providing a dedicated macro for registering a new dimension type and letting it take care of all the repetitive boilerplate for the user, such as promote, convert, registry_defaults!, and so on. That would not only make it easier to extend the dimension system, but also allow the new dimensions to work seamlessly with the existing Dimensions type.
I’ve drafted a rough proposal below for reference:
function _check_dimension_extension(::Type{New}, ::Type{Old}) where {New<:AbstractDimensions,Old<:AbstractDimensions}
new_names, old_names = dimension_names(New), dimension_names(Old)
missing_dims = filter(name -> name ∉ new_names, old_names)
!isempty(missing_dims) && throw(ArgumentError("$(New) cannot extend $(Old): missing dimensions $(missing_dims)"))
nothing
end
@generated function _convert_dimension_extension(::Type{New}, d::Old) where {P,New<:AbstractDimensions{P},Old<:AbstractDimensions}
new_names, old_names = dimension_names(New), dimension_names(Old)
missing_dims = filter(name -> name ∉ new_names, old_names)
!isempty(missing_dims) && error("$(New) cannot extend $(Old): missing dimensions $(missing_dims)")
args = map(new_names) do name
name ∈ old_names ? :(convert($P, getproperty(d, $(QuoteNode(name))))) : :(zero($P))
end
return :(isunknown(d) ? unknown($New) : $New($(args...)))
end
macro register_dimensions(units, new, old)
return esc(quote
FlexUnits.RegistryTools._check_dimension_extension($new, $old)
@inline $new(args::Real...) = $new{FlexUnits.FixRat32}(args...)
@inline Base.promote_rule(::Type{$new{P1}}, ::Type{$new{P2}}) where {P1,P2} =
$new{promote_type(P1, P2)}
@inline Base.promote_rule(::Type{$new{P1}}, ::Type{$old{P2}}) where {P1,P2} =
$new{promote_type(P1, P2)}
@inline Base.promote_rule(::Type{$old{P1}}, ::Type{$new{P2}}) where {P1,P2} =
$new{promote_type(P1, P2)}
@inline (::Type{$new{P}})(d::$old) where P =
FlexUnits.RegistryTools._convert_dimension_extension($new{P}, d)
FlexUnits.RegistryTools.registry_defaults!(units)
uparse(str::String) = FlexUnits.RegistryTools.uparse(str, units)
qparse(str::String) = FlexUnits.RegistryTools.qparse(str, units)
macro u_str(str); return FlexUnits.RegistryTools.suparse_expr(str, units); end
macro ud_str(str); return FlexUnits.RegistryTools.uparse_expr(str, units); end
macro q_str(str); return FlexUnits.RegistryTools.qparse_expr(str, units); end
macro U_str(str); suexpr = FlexUnits.RegistryTools.suparse_expr(str, units); return :(typeof($suexpr)); end
macro D_str(str); suexpr = FlexUnits.RegistryTools.suparse_expr(str, units); return :(FlexUnits.RegistryTools.dimtype($suexpr)); end
utype() = FlexUnits.RegistryTools.regunittype(units)
dtype() = FlexUnits.RegistryTools.regdimtype(units)
nothing
end)
end
With something like this, defining a new dimension set could become very simple:
module MyUnits
using FlexUnits
using FlexUnits.RegistryTools
@kwdef struct MoneyDimensions{P} <: AbstractDimensions{P}
# existing 7 dimensions
EUR::P = zero(FixRat32)
end
const UNITS = PermanentDict{Symbol,Units{MoneyDimensions{FixRat32},AffineTransform{Float64}}}()
@register_dimensions UNITS MoneyDimensions Dimensions
register_unit!(UNITS, "EUR" => MoneyDimensions{FixRat32}(EUR=1))
register_unit!(UNITS, "kEUR" => 1000*UNITS[:EUR])
const CURRENCY_UNITS = [UNITS[k] for k in [:F,:H,:T,:Ω,:V,:W,:J,:Pa,:N,:C,:L,:EUR]]
FlexUnits.preferred_units(::Type{<:MoneyDimensions}) = CURRENCY_UNITS
export @u_str # ...
end