How to dispatch on a type alias?

I have trouble implementing the dispatch I want using type aliases. I want to dispatch on Type{A{T1,B{T3}}} where {T1,T3}, but i do not find the right syntax. I currently have something that looks like :

struct A{T1,T2}
    x::T1
    y::T2
end

struct B{T3}
    z::T3
end
# an alias : 
const C{T1,T3} = A{T1,B{T3}}
# and a constructor : 
C(x,z) = A(x,B(z))


c = C(1,2.)
typeof(c) # returns C{Int64, Float64} (alias for A{Int64, B{Float64}})

function myfunction(::Type{A{T1,B{T3}}}) where {T1,T3}
    return 1
end

myfunction(C) # Errors....

The function already dispatches on e.g. typeof(c), but not on C itself.

The Type{...} type is itself invariant. So you want:

function myfunction(::Type{T}) where {T<:A{T1,B{T3}} where {T1, T3}}
    return 1
end

or even just

myfunction(::Type{C}) = 1
3 Likes

Thanks @mbauman this is working correctly indeed… but it is not what I needed… By my fault, not yours.

With a closer look, the problem is in fact the following:

function second_function(::Type{T}) where {T<:A{T1,G} where {T1, G}}
    # I need to know which type is G :
    return G 
end

The issue is that I need to access the fact that the inner type is “B” from inside the function… and it tells me that

julia> second_function(C)
ERROR: UndefVarError: `G` not defined
Stacktrace:
 [1] second_function(#unused#::Type{C})        
   @ Main .\Untitled-1:20
 [2] top-level scope
   @ Untitled-1:25 

The thing is that I want to dispatch on G inside the function.

Then you “raise” the G type parameter up to the function level (instead of being buried in T):

julia> function second_function(::Type{T}) where {G, T<:A{T1,G} where T1}
           return G
       end
second_function (generic function with 1 method)

julia> second_function(C{1,2})
B{2}
3 Likes

This is it. But now why is’nt second_function(C) working ? I understand that C is not a type but a group of types… But I really only want B and not B{T3} to be returned. Afterall,

julia> C
C (alias for A{T1, B{T3}} where {T1, T3})      

julia> 

So the information that the second slot of C is B{T3} where T3 is there in the object.

I think that’s just a rule of how dispatch with type parameters works — it requires matching against a fully-resolved type. So I don’t think you’ll ever get a type parameter with a where clause inside a function.

So I cannot dispatch on C at all without specifying more (like C{1,2.}) you used ?

Sure you can — it’s just a different method: second_function(::Type{C}) = B{T3} where T3.

1 Like

Yes this is the behavior I want. But I want is for several types at once :

struct B1{T3} 
    y::T3
end
const C1{T1,T3} = A{T1,B1{T3}}

struct B2{T3} 
    y::T3
end
const C2{T1,T3} = A{T1,B2{T3}}

struct B3{T3} 
    y::T3
end
const C3{T1,T3} = A{T1,B3{T3}}

struct B4{T3} 
    y::T3
end
const C4{T1,T3} = A{T1,B4{T3}}

# I have like 12 of them

So IIUC I have to define all the methods:

second_function(::Type{Ci}) = inner_method(Bi)

to be able to simply dispatch on this ?

If yes then this is not a huge problem I can copy/paste

That sure looks like you’re wading into some pretty dark and scary woods… there will surely be more dragons lurking about. I’m afraid I don’t have more concrete suggestions given the sketchy nature here, but it looks fraught to me.

I Will try to setup a more detailed example so that we can start talking reasons for doing that and options to get out of those woods then I think.

I’ve been thinking of it in my head as method where clauses requiring the parameters be specified because they may be matched across multiple arguments or be accessible values in the method body. Reminds me of this past thread, I’ll just share the last small example sans the thread’s context:

As for this particular case:

# works for _(C), fails for _(C{Int, Float64})
function myfunction(::Type{ A{T1,B{T3}} where {T1,T3} })
  return 1
end

# names disambiguated to avoid multimethods
# works for _(C{Int, Float64}), fails for _(C)
function myfunction1(::Type{ A{T1,B{T3}} }) where {T1,T3}
  return 1
end

function myfunction2(::Type{ A{T1,B{T3}} } where {T1, T3})
  return 1
end

As said before, method parameters must be specified, so myfunction1 failing makes sense. myfunction2 fails because the parametric Type requires that the T1 and T3 be specified, in the same way that D = Vector{Ref{T}} where T only includes Vector{Ref{Int}} or Vector{Ref{Float64}}, but not Vector{Ref}.

In fact myfunction1 and myfunction2 would override each other with the same function name because the argument requires specified T1 and T3, despite not being equivalent (T1 and T3 are not available in the method body nor can they matched across arguments).

Well, it does not look like your version solves my problem : I need to avoid rewriting it for B1,B2,B3…

My hunch is that all these types should just be one type, potentially parameterized, but mostly storing the distinctions between them as data in the fields. It’s likely going to be hard to preserve type stability — especially if you want to have functions returning UnionAlls, for example — and then you might as well just use ifs instead of dispatch in any case.

But again, hard to say.

1 Like

Right, I wasn’t addressing that, just how type parameters work. You could avoid manual copies and rewrites with metaprogramming, something like:

for i in 1:4
    # make Symbols representing the variables
    Bi = Symbol('B'*string(i))
    Ci = Symbol('C'*string(i))
    # @eval this block in the global scope
    # @eval allows interpolation of Symbols into the expression
    @eval begin
         struct $(Bi){T3} 
             y::T3
         end
         const $(Ci){T1,T3} = A{T1,$(Bi){T3}}
    end
end

But this is assuming that you are making a MWE that doesn’t show the utility, otherwise I would caution you against making so many types with the same structure and methods, that could just be 1 type.

1 Like

Thanks for the metaprogramming, this is the right solution to meet my criterias indeed. However:

I agree completely with these statements, and therefore if you want to read the following I’ll describe a bit more the issue I am having so that we may or may not conclude on a different API.

I have an abstract Generator type with approx 12 generators implemented (but more expected in the future), with “known” names:

abstract type Generator end
(G::Generator)(x) = # this method should be implemented for all generator. 
# then I have several generator: 
struct ClaytonGenerator <: Generator end
struct GumbelGenerator <: Generator end 
# and all implement the same functor method. 

For simplicity i give them no parameters at all but all have fields in them and are fundamentally different from each other.

Then I have the main type of the problem, that implements something on top of a generator:

struct Archimedean{d, TG}
    G::TG
    function Archimedean(d,G)
        new{d,typeof(G)}(G)
    end
end

This interface is hidden from the user through a set of aliases:

const Clayton{d} = Archimedean{d, ClaytonGenerator}
Clayton(d,...) = Archimedean(d,ClaytonGenerator(...)) # again, there are parameters in the true model

const Gumbel{d} = Archimedean{d, GumbelGenerator}
Gumbel(d,...) = Archimedean(d,GumbelGenerator(...)) # again, there are parameters in the true model

In fact, the users might not even know that the generators exists (but if they do, they are also exposed), and these names are “known” from the litterature.

All those objects are models, that you can fit to a dataset via :

fit(Clayton,data)
fit(Gumbel,data)

At least, that was the interface I had before, when Clayton, Gumbel, ... where their own types <:Archimedean and the Generators were just methods from these types. But now I have another kind of model, that is supposed to take the same place in the structure as the Archimedean type, and leverage the implementation of the generators.

Since the Generator is in facrt the stuff that needs to be fitted, I wanted to devise, from only the Clayton object, that the right generator to use is the ClaytonGenerator.

So there are two goals :

  1. Keep the outside fitting interface that is already there
  2. Move generators out of the Clayton and Gumbel structs to be able to leverage them at other places of the code.

Thaughts ?

Can’t say for sure since I don’t understand the full API, but this sounds similar to a Holy trait.

Ah, then types indeed might be the answer! But in many cases, I’ve found it easier to dispatch on values rather than types, even if those values are just singletons that flag which generator to use.

Hum… Not sure about how it would solve my issue of not being able to find the generator from the alias, since I cannot dispatch on the alias…

I agree. But the fit API is not mine and comes from Distribution Fitting · Distributions.jl… and takes the type as its first input.

I might be misunderstanding something then, I was imagining something like generatorof(::Type{Clayton}) = ClaytonGenerator or generatorof(::Clayton) = ClaytonGenerator, depending on if you are passing Clayton itself or its instances.