f(x::MyType{T, S}) where (T || S) <: Foo?

Hi,

I have a type where I want a different dispatch if one of the parameters is Foo.

abstract type FooBarBaz end

struct Foo <: FooBarBaz end 
struct Bar <: FooBarBaz end
struct Baz <: FooBarBaz end

struct MyType{T<:FooBarBaz, S<:FooBarBaz} end

The cleanest (legal) way I can think of doing this is

f(x::MyType{Foo}) = ...
f(x::MyType{T,Foo}) where T = ...

But I end up in dispatch hell pretty quickly with more parameters.

Is something nice for this?

Many thanks!

I suppose you could make a hasfooparam method so you only have to waddle through dispatch hell once. Do you anticipate having a large number of type parameters?

1 Like
f(x::MyType{<:Any,Foo}) = ...

also Union{Foo, Bar} === Union{Bar, Foo}, if that helps

1 Like

For something a bit more automated, how about this?

julia> fooey = [:(MyType{$(fill(:(<:Any), n)...), Foo}) for n=0:4]
5-element Vector{Expr}:
 :(MyType{Foo})
 :(MyType{<:Any, Foo})
 :(MyType{<:Any, <:Any, Foo})
 :(MyType{<:Any, <:Any, <:Any, Foo})
 :(MyType{<:Any, <:Any, <:Any, <:Any, Foo})

julia> const FooTypes = eval(:(Union{$(fooey...)}))

Then you could do

f(x::FooTypes) = ...

It’s also possible to use the @generated macro to make generated functions…

@generated f(x) = begin
    if #= logic to detect if `Foo` is a parameter of the type `x` =#
        :(#= function code =#)
    else
        :(#= other function code =#)
    end 
end

Edit:

Almost forgot. You can also use switching logic like this, if the performance is acceptable:

f(x::T) where T<:MyType = begin
    if Foo ∈ T.parameters
        #= ... =#
    else
        #= ... =#
    end
end 

The downside being that you lose function signature discoverability.

1 Like

I don’t think this would work as MyType{<:Any, Foo, <:Any} would be rejected

Edit

Oh no! I think you’re right about this one my bad

1 Like

Indeed I do, this is the way I’m handling it now.

hasfoo(x::MyType{T,S,V}) where {T,S,V} = any(map(param -> param <: Foo, (T,S,V)))

As you can imagine, this does not scratch the itch. if hasfoo(x)ing all over the place is also rather unsightly.

You can at least simplify the above to

hasfoo(x::MyType{T,S,V}) where {T,S,V} = any(p -> p <: Foo, (T,S,V))

any accepts a function as a first input, so that any(f, itr) is basically equivalent to any(f(x) for x in itr).

Can you demonstrate how you are using hasfoo in a way that makes it unsatisfactory?

1 Like

Depending on your problem, you might find it appropriate to store the “hasfoo” status of the type as an extra Boolean type parameter which you can dispatch on. You may also write an inner constructor which calculates the extra type parameter so you don’t have to think about it.

For instance:

abstract type FooBarBaz end

struct Foo <: FooBarBaz end 
struct Bar <: FooBarBaz end
struct Baz <: FooBarBaz end

struct MyType{T<:FooBarBaz, S<:FooBarBaz, Fooey}
	MyType(T, S) = let hasfoo = T <: Foo || S <: Foo
		new{T,S,hasfoo}()
	end
end

const MyTypeFooey{T,S} = MyType{T,S,true}

f(::MyTypeFooey) = "this is a Fooey type"
f(::MyType) = "this is a not a Fooey type"

As a bonus, you can define type aliases which display nicely (see below) and make dispatch easier. I’ve defined MyTypeFooey, but you could also define MyTypeFooless and write methods for both types.

julia> fooless = MyType(Bar, Baz)
MyType{Bar, Baz, false}()

julia> f(fooless)
"this is a not a Fooey type"

julia> fooey = MyType(Bar, Foo) # notice how the type is represented
MyTypeFooey{Bar, Foo}()

julia> typeof(fooey)
MyTypeFooey{Bar, Foo} (alias for MyType{Bar, Foo, true})

julia> f(fooey)
"this is a Fooey type"
1 Like

Ohhh I like this approach. I like it a lot. Take my updoot.

1 Like

To me this or condition sounds like the type parameters of MyType actually represent a set of types, i.e., their order would not matter. Thus, you could normalize them in the constructor by sorting your types:

abstract type FooBarBaz end

struct Foo <: FooBarBaz end 
struct Bar <: FooBarBaz end
struct Baz <: FooBarBaz end

Base.isless(x::Type{Foo}, y::Type{Bar}) = true
Base.isless(x::Type{Bar}, y::Type{Foo}) = false
Base.isless(x::Type{Foo}, y::Type{Baz}) = true
Base.isless(x::Type{Baz}, y::Type{Foo}) = false
Base.isless(x::Type{Bar}, y::Type{Baz}) = true
Base.isless(x::Type{Baz}, y::Type{Bar}) = false

struct MyType{T<:FooBarBaz, S<:FooBarBaz}
    function MyType(x, y)
        new{sort([typeof(x), typeof(y)])...}() 
    end
end

julia> MyType(Bar(), Foo())
MyType{Foo, Bar}()

Now, dispatching on the first argument is enough as any Foo type parameter would end up there.

2 Likes

The order does matter unfortunately.

This is nice, I think this approach may be better served by Holy traits?
Still, none of these feel very Julia, I was hoping there was a neat little hack I could stick in the function signature. Seems like there isn’t after all.

Can you demonstrate how you are using hasfoo in a way that makes it unsatisfactory?

f(x::MyType) = 0
f(x::MyType{Bar, Bar} = 1
f(x::MyType{Baz, Baz} = 2
f(x::MyType{Bar, Baz} = 3
f(x::MyType{Baz, Bar} = 4
f(x::MyType{Foo}) = 5
f(x::MyType{<:FooBarBaz, Foo} = 5 #!!Repeated code!!

Becomes

f(x::MyType{Bar, Bar} = 1
f(x::MyType{Baz, Baz} = 2
f(x::MyType{Bar, Baz} = 3
f(x::MyType{Baz, Bar} = 4
f(x::MyType) = hasfoo(x) ?  5 : 0

Obviously messier with actual multi-line functions, helper functions (and a lot of them!).

If this is really the only problem, just use a union type:

const MyTypeFoo = Union{MyType{Foo},MyType{<:FooBarBaz,Foo}}

f(x::MyTypeFoo) = 5

(As originally suggested by @uniment!)

2 Likes

Although from this example, it looks like you can just let

f(x::MyType) = 5

be the default fallback to catch anything with Foo, as all the other variations without Foo are already covered :sweat_smile: