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.
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?
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.
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"
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.
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.