Singleton types vs. instances as type parameters

Let’s say I want to create a family of singleton types, and additional types that might use them as type parameters. The particular example I’m thinking of is a named Variable type, and a type to hold powers of that variable:

struct Variable{Name}
  Variable{N}() where {N} = new{typeassert(N, Symbol)}()
end

I have a choice here: I can either use the variable type as a parameter, or I can use its singleton instance. That is, I can do this:

struct Power{V <: Variable}
  exponent::Int
end

or this:

struct Power{V}
  exponent::Int 

  Power{V}(e::Int) where {V} = new{typeassert(V, Variable)}()
end

The first form seems cleaner and easier to write in this case. However, if I want to use a tuple of variables as parameters, I must use instances rather than types:

julia> struct Monomial{Vars}
       exponents
       end

julia> Monomial{(Variable{:x}, Variable{:y})}
ERROR: TypeError: Type: in parameter, expected Type, got Tuple{DataType,DataType}

julia> Monomial{(Variable{:x}(), Variable{:y}())}
Monomial{(Variable{:x}(), Variable{:y}())}

so for consistency with other types that might need a tuple of parameters, using instances seems preferable. Or perhaps it would be better to do Monomial{Tuple{Variable{:x}, Variable{:y}}}?

My question is: are there other trade-offs that I haven’t brought up here?

For example, are there any efficiency differences between Power{Variable{:x}} and Power{Variable{:x}()}? Or have any of you tried structures like this, and did you form a clear preference? I imagine that the authors of packages like Unitful.jl have faced similar questions.

Thanks!

3 Likes

Oh yes, this question :slight_smile:. Fond memories. In JuliaDiffEq, we had a similar problem: what should

solve(prob,alg)

dispatch on, the type of alg or its instance? We had a month where we:

  1. Went with the type
  2. Went to the instance
  3. Went back to the type
  4. Finally at the instance.

It has stayed with the instance ever sense. Example:

solve(prob,RK4())

The reason is because this is much more flexible. It took me awhile to get here though! Arguments are given here:

The general idea is that, if you’re using the instance, you have the full power of the constructor. It is strictly more powerful while the syntax isn’t much more. Some examples are:

  1. Defaults in the constructor. You might create a whole family of related types for dispatch using a type parameter. I see Variable{:x}. What if you wanted a default variable? Using an instance, you can easily make a default constructor that throws :x in there. If you go with the type, you cannot do such defaults later.

  2. If you want some kinds of relations between the types and values in the parameters, you can enforce that with a constructor.

  3. You can add details (more parameters) without breaking code. If you suddenly need a second parameter, say some number which is normally 1 but 2 is some cool new feature, codes which use Variable{:x} will break when you go to Variable{:x,1}, but again a constructor can handle this by putting a default in there.

  4. You never know if you’ll need fields! You have the option to use them in the future if you use instances, even if it’s just for storing some constants.

There’s probably a bit more too, but that’s the general idea. The flexibility is helpful in the long run.

12 Likes

Thanks, @ChrisRackauckas that’s exactly the kind of information I was looking for! I’ve gone through that same set of permutations myself several times, but the point about handling defaults and additional type parameters makes a lot of sense, and it’s not something I’d considered before. Thank you!

In this discussion How to make a vector of parametric composite types? - #11 by Olivier_Merchiers it also appeared from benchmarks that empty instances were faster than using types.

I remember reading somewhere or being told that it was recommended to use types, rather than instances, but since I don’t have a reference for that, consider it hearsay.

2 Likes

You inadvertently were one that led me to realize that using the types directly was a bad idea :slight_smile:

3 Likes

Ha, apparently one is one’s own best teacher…!

2 Likes

The docs seem to imply this http://docs.julialang.org/en/release-0.5/manual/types/#man-singleton-types
" This is useful for writing methods (especially parametric ones) whose behavior depends on a type that is given as an explicit argument rather than implied by the type of one of its arguments."

I think that sentence just means that it’s useful for things like parse(Int, "0").

Thanks everyone! I ended up choosing the instances approach, and it’s going well so far.

Work is underway here: https://github.com/rdeits/TypedPolynomials.jl/tree/abstract-vars (I’ll post this once it’s in a usable state).

This is a very interesting thread. I too have used both, oscillating between the two.

I believe the place where this advice was dispenced was begins in this point of this github thread: https://github.com/JuliaLang/julia/pull/9452#issuecomment-68528662

I have actually come full circle since then (at least once) - and now I believe instances are more powerful and flexible. for traits and singletons. In fact, that discussion seems to predate @pure - it might be worth reviewing Val(x)::Val{x} in this light (that’s easy syntax for both the caller and callee).

Thank you @rdeits for showing the typeassert trick - that’s very neat, I usually do these type parameter assertions in a cluncky way.

2 Likes