Restrictive Type Aliases?

Hi,

I’m trying to define a type alias in a way that is actually restrictive. For instance, distance travelled = rate * time, where all 3 (distances, rates, and times) are inherently floats. I want to define 3 types: a “distance,” a “rate,” and a “time,” each of which is “really” just a float. However, I want to create a function (for instance, “distance_travelled”) that must take exactly one rate, and then exactly one time. Now this particular example might be doable with primitive types, except that I actually want vectors of floats, not just floats, for each of the three types.

For now, my workaround is to define three structs, each of which has just a single component - a vector of floats. But I’m concerned about possible performance losses from retrieving values from those structs, as well as just lack of elegance.

Perhaps I’m missing something quite straightforward - I apologize if that’s the case.

Thanks

2 Likes

Structs should work, and the performance overhead shouldn’t be too bad as immutable containers in most cases should be elided at compile time.

Or you could just use Unitful.jl:

``````julia> import Unitful: km, hr

julia> rate = 60km/hr
60 hr^-1 km

julia> time = 3.14hr
3.14 hr

julia> rate*time
188.4 km
``````

Vectors of these should mostly just work as well.

6 Likes

`function x(z::Array{Float64,1})` will restrict the input arg to a 1d array of floats
Otherwise the `.()` function variant could be used

``````function vdist(xrate::Float64, xtime::Float64)
return xrate * xtime;
end;

r1 = collect(1:1.3:10);
t1 = r1[end:-1:1];

d1 = vdist.(r1, t1);``````

Don’t be. There is little to no overhead in accessing a field from a struct, as long as that field has a concrete type in the struct definition. Julia is less like Python (in which even looking up a single field involves accessing a dictionary) and more like C (in which structs exist only to help the programmer and have little to no runtime cost) in this regard.

3 Likes

Had the same problem, for dimensionless quantities (relative dielectric function: epsilon, refractive index: n).
Unitful.jl seems overkill here (and does not work under 0.7 yet), since these quantities are basically complex numbers.

A single field struct is ugly.

It does not seem possible to subtype from a concrete type, as explained here.

So let’s define a primitive type such as
`primitive type Float_n <: AbstractFloat 64 end`
using `reinterpret` as explained in https://stackoverflow.com/a/49582867/3565696
allows back and forth conversions between `Float64` and the custom `Float_n`.

``````# base Float type for refractive index n
primitive type Float_n  <: AbstractFloat 64 end
Float_n(x::Float64) = reinterpret(Float_n, x)
# back to Float64
Float64(x::Float_n) = reinterpret(Float64, x)
# allow the REPL to show values of type Float_n
Base.show(io::IO, x::Float_n) = print(io, Float64(x))
Base.:+(x::Float_n, y::Float_n) = Float_n(Float64(x) + Float64(y))
Base.:-(x::Float_n, y::Float_n) = Float_n(Float64(x) - Float64(y))
Base.:*(x::Float_n, y::Float_n) = Float_n(Float64(x) * Float64(y))
Base.:/(x::Float_n, y::Float_n) = Float_n(Float64(x) / Float64(y))
# In my application it makes sense for mixed arguments operations to yield Float64
# it is always possible to explicitly reinterpret the result back to Float_n
Base.promote_rule(::Type{Float_n}, ::Type{Float64}) = Float64

julia> Float_n(1.0)
1.0

julia> typeof(ans)
Float_n
``````

preliminary benchmarks look fine:

``````julia> using BenchmarkTools
julia> @btime Float_n(1.0) + Float_n(2.0)
0.014 ns (0 allocations: 0 bytes)
julia> @btime 1.0 + 2.0
0.014 ns (0 allocations: 0 bytes)
@btime Float_n(1.0) + 2.0
0.014 ns (0 allocations: 0 bytes)
``````

This is not perfect:

``````julia> a = rand(Float_n, 10000);
ERROR: ArgumentError: Sampler for this object is not defined
``````

It would be nice for `Float_n` to work as `Float64` when a method does not match.

2 Likes

You would need to define

``````Base.rand(::Type{Float_n}, ...)
``````

or, in `v0.7`, you can use the `Sampler` mechanism.

I don’t think it is possible, and I don’t think it would be nice either, as `Float_n` above violates some basic assumptions about arithmetic, eg:

so an unsuspecting user would get unexpected behavior.

Good catch, wrong copy/paste. Edited now, because it would be distracting. Thanks !

I’ve known it was possible to define ones own primitive types but I’ve never seen it done before.
Nice.

I’m not sure that doing so is a good idea, but ~shrug~.

I feel like there has been a discussion of such a restrictive type alias before.
I think it would be useful.

The properties one wants really are that
for `XX` being a restrictive type alias of `X`,
then `XX` is the closest subtype of `X`, and that `X` can be a concrete type.
So `foo(::XX)` would be considered more specific than `foo(::X)` for dispatch purposes.
You could even reasonably chain them I guess, making `XXX` a restrictive type alias of `XX`.

1 Like

I am assuming that it will be fixed eventually for v0.7. I am not sure in what sense you consider it “overkill”, I think it is a convenient solution with nearly zero additional runtime cost (except for a bit of compilation of course).

Also, unless I want to twiddle bits, I would just define a wrapper type.

1 Like

After reading twice the documentation, it looks like a complex package,
and I am still unsure how to use it for these dimensionless quantities.
I’ll try again when it is 0.7-ready.

What is a “wrapper type” ? How would you do that ?

Me neither, just experimenting

This issue (already cited) was interesting

but in my use case, the fragility does not matter.

Exactly.
This would be greatly eased if defining a fallback type were possible.
(if a method is not found for Float_n, fall back to Float64)

``````struct Float_n
val::Float64
end

Base.:+(x::Float_n, y::Float_n) = Float_n(x.val + y.val)
``````

etc.