Work around invariance

I am working with a family of models, which have certain properties, lets call them statenames and obsnames. Many of these are shared between models, so it would be nice to reuse them. Models are mapped to types in my code. I would like to query these properties for the types.

So I thought I would use abstract types like this:

abstract AbstractModel{S,O}
abstract FiveStateModel
statenames(::Type{FiveStateModel}) = (:NL,:NM,:NH,:EM,:EH)
abstract ObservedEUH
obsnames(::Type{ObservedEUH}) = (:E,:U,:H)

immutable SimpleModel1{T <: Real} <: AbstractModel{FiveStateModel, ObservedEUH}
    x::T
    y::T
end

Now I want

statenames(SimpleModel1)

to return (:NL,:NM,:NH,:EM,:EH). If types were covariant, I could use

statenames{S,O}(::Type{AbstractModel{S,O}}) = statenames(S)
obsnames{S,O}(::Type{AbstractModel{S,O}}) = obsnames(O)

but since they are not, I am using

statenames{S,O}(::AbstractModel{S,O}) = statenames(S)
obsnames{S,O}(::AbstractModel{S,O}) = obsnames(O)

and have to instantiate a SimpleModel1, eg

statenames(SimpleModel1(1.0,1.0))

Is there a workaround that would allow me to solve this in type-space?

Traits. Put a trait on SimpleModel1 and do trait dispatch instead. I would suggest using SimpleTraits.jl to make it easier.

Or just wait for v0.6. It looks like Jeff’s PR is going to make it and so I think that will fix the problem.

Just out of curiosity, which PR is that?

The following approach doesn’t always work, depending on if your subtypes leave the type-parameters of AbstractModel flexible. It looks like you don’t, however, so in your use-case this should do the trick:

# do these as you described
statenames{S,O}(::Type{AbstractModel{S,O}}) = statenames(S)
obsnames{S,O}(::Type{AbstractModel{S,O}}) = obsnames(O)
julia> statenames{T<:AbstractModel}(::Type{T}) = statenames(supertype(T))
statenames (generic function with 3 methods)

julia> statenames(SimpleModel1)
(:NL,:NM,:NH,:EM,:EH)

See the triangular dispatch example. With that extra level of dispatch control you should be able to do what you need.

I found that for my use case, I can simply do

# reuse these for various models
const OBSERVED_EUH = (:E, :U, :H)
const FIVE_STATES = (:NL,:NM,:NH,:EM,:EH)

# and for one specific model
statenames(::SimpleModel1) = FIVE_STATES
obsnames(::SimpleModel1) = OBSERVED_EUH

I guess the lesson is that not all information needs to be encoded in the type system.

Well, that’s still using the type system (via dispatch). It’s that not all information needs to be encoded as fields. In fact, using dispatches to hold information is precisely how traits are implemented, so this and traits are one and the same.

I admit that I am just learning about traits, but from most examples it seems that there is an extra level of indirection involved.