Restrictive Type Aliases?


#1

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

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.


#3

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);

#4

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.

And if you’re really concerned about performance, https://github.com/JuliaCI/BenchmarkTools.jl is your friend :slightly_smiling_face:


#5

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))
# overload operators
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.


#6

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.


#7

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


#8

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.


#9

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.


#10

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 ?


#11

Me neither, just experimenting :wink:

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)


#12
struct Float_n
    val::Float64
end

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

etc.