Skip to content

Commit 548bf22

Browse files
authored
Merge pull request #72 from SymbolicML/construction-base
Clean up type utilities: `constructorof`, `with_type_parameters`, `dimension_names`
2 parents a06bba8 + aec9a12 commit 548bf22

File tree

8 files changed

+128
-44
lines changed

8 files changed

+128
-44
lines changed

docs/make.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using DynamicQuantities
2-
import DynamicQuantities.Units
2+
using DynamicQuantities.Units
3+
using DynamicQuantities: constructorof, with_type_parameters, dimension_names
34
using Documenter
45

56
DocMeta.setdocmeta!(DynamicQuantities, :DocTestSetup, :(using DynamicQuantities); recursive=true)

docs/src/types.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,15 @@ AbstractGenericQuantity
4343
UnionAbstractQuantity
4444
DynamicQuantities.ABSTRACT_QUANTITY_TYPES
4545
```
46+
47+
## Custom behavior in abstract quantities
48+
49+
There are a few functions you may need to overload
50+
when subtyping `AbstractDimensions`, `AbstractQuantity`,
51+
or `AbstractGenericQuantity`.
52+
53+
```@docs
54+
constructorof
55+
with_type_parameters
56+
dimension_names
57+
```

src/arrays.jl

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ struct QuantityArray{T,N,D<:AbstractDimensions,Q<:UnionAbstractQuantity{T,D},V<:
4747
dimensions::D
4848

4949
function QuantityArray(v::_V, d::_D, ::Type{_Q}) where {_T,_N,_D<:AbstractDimensions,_Q<:UnionAbstractQuantity,_V<:AbstractArray{_T,_N}}
50-
Q_out = constructor_of(_Q){_T,_D}
50+
Q_out = with_type_parameters(_Q, _T, _D)
5151
return new{_T,_N,_D,Q_out,_V}(v, d)
5252
end
5353
end
@@ -89,15 +89,15 @@ function Base.convert(::Type{QA}, A::QA) where {QA<:QuantityArray}
8989
return A
9090
end
9191
function Base.convert(::Type{QA1}, A::QA2) where {QA1<:QuantityArray,QA2<:QuantityArray}
92-
Q = quantity_type(QA1)
9392
V = array_type(QA1)
94-
N = ndims(QA1)
95-
96-
raw_array = Base.Fix1(convert, Q).(A)
97-
output = QuantityArray(convert(constructor_of(V){Q,N}, raw_array))
98-
# TODO: This will mess with static arrays
93+
D = dim_type(QA1)
94+
Q = quantity_type(QA1)
9995

100-
return output::QA1
96+
return QuantityArray(
97+
convert(V, ustrip(A)),
98+
convert(D, dimension(A)),
99+
Q,
100+
)::QA1
101101
end
102102

103103
@inline ustrip(A::QuantityArray) = A.value

src/internal_utils.jl

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,6 @@ This file contains utility functions that are not specific to the
33
library, but are used throughout.
44
"""
55

6-
@generated function fieldnames_equal(::Type{T1}, ::Type{T2}) where {T1,T2}
7-
# Needs to be a generated function to ensure hardcoded
8-
return Base.propertynames(T1) == Base.propertynames(T2)
9-
end
10-
116
const SUPERSCRIPT_MAPPING = ('', '¹', '²', '³', '', '', '', '', '', '')
127
const INTCHARS = ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9')
138

src/symbolic_dimensions.jl

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ struct SymbolicDimensions{R} <: AbstractDimensions{R}
3939
nzvals::Vector{R}
4040
end
4141

42-
static_fieldnames(::Type{<:SymbolicDimensions}) = ALL_SYMBOLS
42+
@inline dimension_names(::Type{<:SymbolicDimensions}) = ALL_SYMBOLS
4343
function Base.getproperty(d::SymbolicDimensions{R}, s::Symbol) where {R}
4444
nzdims = getfield(d, :nzdims)
4545
i = get(ALL_MAPPING, s, INDEX_TYPE(0))
@@ -53,7 +53,9 @@ function Base.getproperty(d::SymbolicDimensions{R}, s::Symbol) where {R}
5353
end
5454
Base.propertynames(::SymbolicDimensions) = ALL_SYMBOLS
5555
Base.getindex(d::SymbolicDimensions, k::Symbol) = getproperty(d, k)
56-
constructor_of(::Type{<:SymbolicDimensions}) = SymbolicDimensions
56+
57+
constructorof(::Type{<:SymbolicDimensions}) = SymbolicDimensions
58+
with_type_parameters(::Type{<:SymbolicDimensions}, ::Type{R}) where {R} = SymbolicDimensions{R}
5759

5860
SymbolicDimensions{R}(d::SymbolicDimensions) where {R} = SymbolicDimensions{R}(getfield(d, :nzdims), convert(Vector{R}, getfield(d, :nzvals)))
5961
SymbolicDimensions(; kws...) = SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}(; kws...)
@@ -71,7 +73,7 @@ end
7173
for (type, _, _) in ABSTRACT_QUANTITY_TYPES
7274
@eval begin
7375
function Base.convert(::Type{Q}, q::UnionAbstractQuantity{<:Any,<:Dimensions}) where {T,Q<:$type{T,SymbolicDimensions}}
74-
return convert(constructor_of(Q){T,SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}}, q)
76+
return convert(with_type_parameters(Q, T,SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}), q)
7577
end
7678
function Base.convert(::Type{Q}, q::UnionAbstractQuantity{<:Any,<:Dimensions}) where {T,R,Q<:$type{T,SymbolicDimensions{R}}}
7779
syms = (:m, :kg, :s, :A, :K, :cd, :mol)
@@ -82,14 +84,14 @@ for (type, _, _) in ABSTRACT_QUANTITY_TYPES
8284
permute!(I, p)
8385
permute!(V, p)
8486
dims = SymbolicDimensions{R}(I, V)
85-
return constructor_of(Q)(convert(T, ustrip(q)), dims)
87+
return constructorof(Q)(convert(T, ustrip(q)), dims)
8688
end
8789
function Base.convert(::Type{Q}, q::UnionAbstractQuantity{<:Any,<:SymbolicDimensions}) where {T,D<:Dimensions,Q<:$type{T,D}}
88-
result = constructor_of(Q)(T(ustrip(q)), D())
90+
result = constructorof(Q)(convert(T, ustrip(q)), D())
8991
d = dimension(q)
9092
for (idx, value) in zip(getfield(d, :nzdims), getfield(d, :nzvals))
9193
if !iszero(value)
92-
result = result * convert(constructor_of(Q){T,D}, ALL_VALUES[idx]) ^ value
94+
result = result * convert(with_type_parameters(Q, T, D), ALL_VALUES[idx]) ^ value
9395
end
9496
end
9597
return result
@@ -108,7 +110,7 @@ for converting to specific symbolic units, or `convert(Quantity{<:Any,<:Symbolic
108110
for assuming SI units as the output symbols.
109111
"""
110112
function uexpand(q::Q) where {T,R,D<:SymbolicDimensions{R},Q<:UnionAbstractQuantity{T,D}}
111-
return convert(constructor_of(Q){T,Dimensions{R}}, q)
113+
return convert(with_type_parameters(Q, T, Dimensions{R}), q)
112114
end
113115
uexpand(q::QuantityArray) = uexpand.(q)
114116
# TODO: Make the array-based one more efficient

src/types.jl

Lines changed: 67 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import Tricks: static_fieldnames, static_fieldtypes
1+
using Tricks: static_fieldnames
22

33
const DEFAULT_DIM_BASE_TYPE = FixedRational{DEFAULT_NUMERATOR_TYPE,DEFAULT_DENOM}
44
const DEFAULT_VALUE_TYPE = Float64
@@ -17,7 +17,7 @@ the need to define many other functions.
1717
The key function that one could wish to overload is
1818
`DynamicQuantities.dimension_name(::AbstractDimensions, k::Symbol)` for mapping from a field name
1919
to a base unit (e.g., `length` by default maps to `m`). You may also need to overload
20-
`DynamicQuantities.constructor_of(::Type{T})` in case of non-standard construction.
20+
`constructorof(::Type{T})` in case of non-standard construction.
2121
"""
2222
abstract type AbstractDimensions{R} end
2323

@@ -103,13 +103,13 @@ struct Dimensions{R<:Real} <: AbstractDimensions{R}
103103
amount::R
104104
end
105105

106-
(::Type{D})(::Type{R}; kws...) where {R,D<:AbstractDimensions} = constructor_of(D){R}((tryrationalize(R, get(kws, k, zero(R))) for k in static_fieldnames(D))...)
107-
(::Type{D})(; kws...) where {R,D<:AbstractDimensions{R}} = constructor_of(D)(R; kws...)
106+
(::Type{D})(::Type{R}; kws...) where {R,D<:AbstractDimensions} = with_type_parameters(D, R)((tryrationalize(R, get(kws, k, zero(R))) for k in dimension_names(D))...)
107+
(::Type{D})(; kws...) where {R,D<:AbstractDimensions{R}} = constructorof(D)(R; kws...)
108108
(::Type{D})(; kws...) where {D<:AbstractDimensions} = D(DEFAULT_DIM_BASE_TYPE; kws...)
109109
function (::Type{D})(d::D2) where {R,D<:AbstractDimensions{R},D2<:AbstractDimensions}
110-
fieldnames_equal(D, D2) ||
110+
dimension_names_equal(D, D2) ||
111111
error("Cannot create a dimensions of `$(D)` from `$(D2)`. Please write a custom method for construction.")
112-
D((getproperty(d, k) for k in static_fieldnames(D))...)
112+
D((getproperty(d, k) for k in dimension_names(D))...)
113113
end
114114

115115
const DEFAULT_DIM_TYPE = Dimensions{DEFAULT_DIM_BASE_TYPE}
@@ -175,28 +175,79 @@ const ABSTRACT_QUANTITY_TYPES = ((AbstractQuantity, Number, Quantity), (Abstract
175175

176176
for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES
177177
@eval begin
178-
(::Type{Q})(x::T, ::Type{D}; kws...) where {D<:AbstractDimensions,T<:$base_type,T2,Q<:$type{T2}} = constructor_of(Q)(convert(T2, x), D(; kws...))
179-
(::Type{Q})(x::$base_type, ::Type{D}; kws...) where {D<:AbstractDimensions,Q<:$type} = constructor_of(Q)(x, D(; kws...))
180-
(::Type{Q})(x::T; kws...) where {T<:$base_type,T2,Q<:$type{T2}} = constructor_of(Q)(convert(T2, x), dim_type(Q)(; kws...))
181-
(::Type{Q})(x::$base_type; kws...) where {Q<:$type} = constructor_of(Q)(x, dim_type(Q)(; kws...))
178+
(::Type{Q})(x::T, ::Type{D}; kws...) where {D<:AbstractDimensions,T<:$base_type,T2,Q<:$type{T2}} = constructorof(Q)(convert(T2, x), D(; kws...))
179+
(::Type{Q})(x::$base_type, ::Type{D}; kws...) where {D<:AbstractDimensions,Q<:$type} = constructorof(Q)(x, D(; kws...))
180+
(::Type{Q})(x::T; kws...) where {T<:$base_type,T2,Q<:$type{T2}} = constructorof(Q)(convert(T2, x), dim_type(Q)(; kws...))
181+
(::Type{Q})(x::$base_type; kws...) where {Q<:$type} = constructorof(Q)(x, dim_type(Q)(; kws...))
182182
end
183183
for (type2, _, _) in ABSTRACT_QUANTITY_TYPES
184184
@eval begin
185-
(::Type{Q})(q::$type2) where {T,D<:AbstractDimensions,Q<:$type{T,D}} = constructor_of(Q)(convert(T, ustrip(q)), convert(D, dimension(q)))
186-
(::Type{Q})(q::$type2) where {T,Q<:$type{T}} = constructor_of(Q)(convert(T, ustrip(q)), dimension(q))
187-
(::Type{Q})(q::$type2) where {Q<:$type} = constructor_of(Q)(ustrip(q), dimension(q))
185+
(::Type{Q})(q::$type2) where {T,D<:AbstractDimensions,Q<:$type{T,D}} = constructorof(Q)(convert(T, ustrip(q)), convert(D, dimension(q)))
186+
(::Type{Q})(q::$type2) where {T,Q<:$type{T}} = constructorof(Q)(convert(T, ustrip(q)), dimension(q))
187+
(::Type{Q})(q::$type2) where {Q<:$type} = constructorof(Q)(ustrip(q), dimension(q))
188188
end
189189
end
190190
end
191191

192192
const DEFAULT_QUANTITY_TYPE = Quantity{DEFAULT_VALUE_TYPE, DEFAULT_DIM_TYPE}
193193

194-
new_dimensions(::Type{D}, dims...) where {D<:AbstractDimensions} = constructor_of(D)(dims...)
195-
new_quantity(::Type{Q}, l, r) where {Q<:UnionAbstractQuantity} = constructor_of(Q)(l, r)
194+
new_dimensions(::Type{D}, dims...) where {D<:AbstractDimensions} = constructorof(D)(dims...)
195+
new_quantity(::Type{Q}, l, r) where {Q<:UnionAbstractQuantity} = constructorof(Q)(l, r)
196196

197197
dim_type(::Type{Q}) where {T,D<:AbstractDimensions,Q<:UnionAbstractQuantity{T,D}} = D
198198
dim_type(::Type{<:UnionAbstractQuantity}) = DEFAULT_DIM_TYPE
199-
constructor_of(::Type{T}) where {T} = Base.typename(T).wrapper
199+
200+
"""
201+
constructorof(::Type{<:AbstractDimensions})
202+
constructorof(::Type{<:UnionAbstractQuantity})
203+
204+
Return the constructor of the given type. This is used to create new objects
205+
of the same type as the input. Overload a method for a new type, especially
206+
if you need custom behavior.
207+
"""
208+
constructorof(::Type{<:Dimensions}) = Dimensions
209+
constructorof(::Type{<:Quantity}) = Quantity
210+
constructorof(::Type{<:GenericQuantity}) = GenericQuantity
211+
212+
"""
213+
with_type_parameters(::Type{<:AbstractDimensions}, ::Type{R})
214+
with_type_parameters(::Type{<:UnionAbstractQuantity}, ::Type{T}, ::Type{D})
215+
216+
Return the type with the given type parameters instead of the ones in the input type.
217+
This is used to get `Dimensions{R}` from input `(Dimensions{R1}, R)`, for example.
218+
Overload a method for a new type, especially if you need custom behavior.
219+
"""
220+
function with_type_parameters(::Type{<:Dimensions}, ::Type{R}) where {R}
221+
return Dimensions{R}
222+
end
223+
function with_type_parameters(::Type{<:Quantity}, ::Type{T}, ::Type{D}) where {T,D}
224+
return Quantity{T,D}
225+
end
226+
function with_type_parameters(::Type{<:GenericQuantity}, ::Type{T}, ::Type{D}) where {T,D}
227+
return GenericQuantity{T,D}
228+
end
229+
230+
# The following functions should be overloaded for special types
231+
function constructorof(::Type{T}) where {T<:Union{UnionAbstractQuantity,AbstractDimensions}}
232+
return Base.typename(T).wrapper
233+
end
234+
function with_type_parameters(::Type{D}, ::Type{R}) where {D<:AbstractDimensions,R}
235+
return constructorof(D){R}
236+
end
237+
function with_type_parameters(::Type{Q}, ::Type{T}, ::Type{D}) where {Q<:UnionAbstractQuantity,T,D}
238+
return constructorof(Q){T,D}
239+
end
240+
241+
"""
242+
dimension_names(::Type{<:AbstractDimensions})
243+
244+
Return a tuple of symbols with the names of the dimensions of the given type.
245+
This should be static so that it can be hardcoded during compilation.
246+
The default is to use `fieldnames`, but you can overload this for custom behavior.
247+
"""
248+
@inline function dimension_names(::Type{D}) where {D<:AbstractDimensions}
249+
return static_fieldnames(D)
250+
end
200251

201252
struct DimensionError{Q1,Q2} <: Exception
202253
q1::Q1

src/utils.jl

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
import Tricks: static_fieldnames
21
import Compat: allequal
32

43
function map_dimensions(f::F, args::AbstractDimensions...) where {F<:Function}
54
dimension_type = promote_type(typeof(args).parameters...)
6-
dimension_names = static_fieldnames(dimension_type)
5+
dim_names = dimension_names(dimension_type)
76
return new_dimensions(
87
dimension_type,
98
(
109
f((getproperty(arg, dim) for arg in args)...)
11-
for dim in dimension_names
10+
for dim in dim_names
1211
)...
1312
)
1413
end
@@ -48,9 +47,14 @@ function Base.promote_rule(::Type{<:AbstractQuantity}, ::Type{<:Number})
4847
return Number
4948
end
5049

51-
Base.keys(d::AbstractDimensions) = static_fieldnames(typeof(d))
50+
Base.keys(d::AbstractDimensions) = dimension_names(typeof(d))
5251
Base.getindex(d::AbstractDimensions, k::Symbol) = getfield(d, k)
5352

53+
@generated function dimension_names_equal(::Type{T1}, ::Type{T2}) where {T1,T2}
54+
# Needs to be a generated function to ensure hardcoded
55+
return dimension_names(T1) == dimension_names(T2)
56+
end
57+
5458
# Compatibility with `.*`
5559
Base.size(q::UnionAbstractQuantity) = size(ustrip(q))
5660
Base.length(q::UnionAbstractQuantity) = length(ustrip(q))
@@ -116,8 +120,8 @@ end
116120
for f in (:one, :typemin, :typemax)
117121
@eval begin
118122
Base.$f(::Type{Q}) where {T,D,Q<:UnionAbstractQuantity{T,D}} = new_quantity(Q, $f(T), D)
119-
Base.$f(::Type{Q}) where {T,Q<:UnionAbstractQuantity{T}} = $f(constructor_of(Q){T, DEFAULT_DIM_TYPE})
120-
Base.$f(::Type{Q}) where {Q<:UnionAbstractQuantity} = $f(Q{DEFAULT_VALUE_TYPE, DEFAULT_DIM_TYPE})
123+
Base.$f(::Type{Q}) where {T,Q<:UnionAbstractQuantity{T}} = $f(with_type_parameters(Q, T, DEFAULT_DIM_TYPE))
124+
Base.$f(::Type{Q}) where {Q<:UnionAbstractQuantity} = $f(with_type_parameters(Q, DEFAULT_VALUE_TYPE, DEFAULT_DIM_TYPE))
121125
end
122126
if f == :one # Return empty dimensions, as should be multiplicative identity.
123127
@eval Base.$f(q::Q) where {Q<:UnionAbstractQuantity} = new_quantity(Q, $f(ustrip(q)), one(dimension(q)))

test/unittests.jl

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,13 @@ end
519519
@test typeof(MyDimensions(1, 1, 1)) == MyDimensions{Int}
520520
@test typeof(MyDimensions{Float64}(1, 1, 1)) == MyDimensions{Float64}
521521

522+
# Can use the default constructorof, with_type_parameters, and dimension_names:
523+
@test DynamicQuantities.constructorof(MyDimensions{Float64}) == MyDimensions
524+
@test DynamicQuantities.constructorof(MyQuantity{Float64}) == MyQuantity
525+
@test DynamicQuantities.with_type_parameters(MyDimensions{Float64}, Rational{Int}) == MyDimensions{Rational{Int}}
526+
@test DynamicQuantities.with_type_parameters(MyQuantity{Float64,DEFAULT_DIM_TYPE}, Float32, MyDimensions{Float64}) == MyQuantity{Float32,MyDimensions{Float64}}
527+
@test DynamicQuantities.dimension_names(MyDimensions{Float64}) == (:length, :mass, :time)
528+
522529
# But, we always need to use a quantity when mixing with mathematical operations:
523530
@test_throws ErrorException MyQuantity(0.1) + 0.1 * MyDimensions()
524531
end
@@ -545,7 +552,7 @@ end
545552
end
546553

547554
# Internal constructor
548-
@test DynamicQuantities.constructor_of(typeof(sym)) === SymbolicDimensions
555+
@test DynamicQuantities.constructorof(typeof(sym)) === SymbolicDimensions
549556

550557
# Equality comparisons
551558
@test sym == sym
@@ -625,6 +632,7 @@ end
625632
y = 0.5us"km/s"
626633
qa = [x, y]
627634
@test qa isa Vector{Quantity{Float64,SymbolicDimensions{Rational{Int}}}}
635+
DynamicQuantities.with_type_parameters(SymbolicDimensions{Float64}, Rational{Int}) == SymbolicDimensions{Rational{Int}}
628636
end
629637

630638
@testset "uconvert" begin
@@ -1069,4 +1077,15 @@ end
10691077
# TODO: Currently this converts to a `Vector` of `GenericQuantity`
10701078
@test_skip x .* z isa QuantityArray{Float32,1,<:Dimensions,<:GenericQuantity{Float32}}
10711079
end
1080+
1081+
@testset "Array conversion" begin
1082+
x = SArray{Tuple{3}}(randn(3))
1083+
y = SArray{Tuple{3}}(randn(Float32, 3))
1084+
qx = QuantityArray(x, Dimensions(Rational{Int}, length=1))
1085+
qy = QuantityArray(y; length=1)
1086+
1087+
@test typeof(convert(typeof(qx), qy)) == typeof(qx)
1088+
@test convert(typeof(qx), qy)[1] isa Quantity{Float64}
1089+
@test convert(typeof(qx), qy)[1] == convert(Quantity{Float64}, qy[1])
1090+
end
10721091
end

0 commit comments

Comments
 (0)