Unitful.jl
Unitful.jl copied to clipboard
Parameter Type Stability on Unitful.jl parametric types
Currently Unitful.jl parametric type definitions lack indication/restrictions of what the type parameters are expected to be, for instance, src/types.jl defines Quantity{T,D,U} as:
struct Quantity{T,D,U} <: AbstractQuantity{T,D,U}
val::T
Quantity{T,D,U}(v::Number) where {T,D,U} = new{T,D,U}(v)
Quantity{T,D,U}(v::Quantity) where {T,D,U} = convert(Quantity{T,D,U}, v)
end
By building Quantity types by the intended interface, we finally know what {T,D,U} are expected to be—a numeric Type, a Dimensions Value, and a Units Type, respectively:
julia> typeof(1u"m")
Quantity{Int64,𝐋,Unitful.FreeUnits{(m,),𝐋,nothing}}
julia> ptypes(x::Quantity{T,D,U}) where {T,D,U} = map(typeof, (T, D, U))
ptypes (generic function with 1 method)
julia> ptypes(1u"m")
(DataType, Unitful.Dimensions{(Unitful.Dimension{:Length}(1//1),)}, DataType)
But this design allows for the creation of "broken" instances:
julia> broken=Quantity{Float64,Float64,Float64}(3.0);
julia> typeof(broken)
Quantity{Float64,Float64,Float64}
In this example, broken cannot be printed, but can be wrongly instantiated.
This could be avoided if, for instance, each supertype parameter had type annotations in them, for instance (not necessarily a suggested implementation):
abstract type myAbstractQuantity{T<:Union{Real,Complex}, D<:Unitful.Dimensions, U<:Unitful.Units} end
struct myQuantity{T,D,U} <: myAbstractQuantity{T,D,U}
val::T
myQuantity{T,D,U}(v::Number) where {T,D,U} = new{T,D,U}(v)
# etc...
end
julia> myQuantity{Float64} # Should be OK
myQuantity{Float64,D,U} where U where D
julia> myQuantity{Float64,Float64} # Should indeed error
ERROR: TypeError: in myAbstractQuantity, in D, expected D<:Unitful.Dimensions, got Type{Float64}
Stacktrace:
[1] top-level scope at REPL[22]:1
I also think that such type annotations make it easier to read the code and hence to write packages that use Unitful.jl.
There's currently no way in Julia to restrict the type of a value being used as a type parameter. You can't write Quantity{T<:Real, D :: Unitful.Dimensions, U<:Unitful.Units}, and D<:Unitful.Dimensions would not have the desired behavior either. So, as for D, there's really no way to restrict it.
More importantly, it's not a great idea to restrict types unnecessarily. As you've written it, T<:Union{Real, Complex} would prevent Unitful from working with the great Measurements.jl package, which defines its own numeric types with uncertainty. I don't think you can really know in advance what numeric types people might come up with, and even if you did, those types would be defined in other packages that you'd have to add as dependencies in order to explicitly reference them. So T shouldn't be restricted either.
So, I guess I'm against adding type restrictions, but on the other hand, if you feel the documentation could be improved for package developers, I would welcome a pull request.
T<:Union{Real, Complex}would prevent Unitful from working with the great Measurements.jl package, which defines its own numeric types with uncertainty.
Not really, as Measurements.Measurement <: AbstractFloat <: Real:
julia> supertype(Measurements.Measurement)
AbstractFloat
So, I guess I'm against adding type restrictions
OK. I see a design choice here. However, one can argue that adding Abstract type restrictions is not too narrow a restriction while at the same time indicating something like: "we don't mean to have Anything here other than Numbers (or Units)", depending on the parameter, T or U.
—x—
As a side comment, the only reason I wrote T<:Union{Real, Complex} instead of T<:Number is because Quantity's supertype is defined as AbstractQuantity <: Number, thus by allowing Quantity{T<:Number,... allows for having Quantity{Quantity{...}...}, which seems weird.
To chime in here I have to say that something seeming weird is less of a problem than someone trying to do something sensible but being prevented from doing it by the type system. Julia really errs on the side of permissiveness as it's a language for rapidly exploring possibilities, not for making rock solid programs guaranteed by the type system (more the use case of Haskel or Rust).