Parametric types & multiple dispatch

Method choice can be determined by a parametric type

struct X{x} end

f(::X{:x1}) = 1
f(::X{:x2}) = 2

f(X{:x1}()) # 1
f(X{:x2}()) # 2

Why not allow it to be determined just with the parameter ?

f({:x1}) = 1
f({:x2}) = 2

f({:x1}) # 1
f({:x1}) # 2

Strictly speaking, it’s no longer a parameter because there is nothing to parameterize. It’s not consistent with the language to use curly braces for anything besides parameterizing types and related where clauses, nor to dispatch methods by instances instead of types. This is more like a feature of pattern-matching (Haskell example) than type-based polymorphism. (Incidentally, you can use type classes to slightly do multiple dispatch in Haskell, but without the convenient syntax or subtyping that we’re familiar with in Julia. Different language designs, different tradeoffs.)

As for a practical reason, dispatching on instances instead of types either invites more performance-ruining compilation bloat or optimization-impeding runtime dispatch, you don’t want this very often. As you know already, the standard Val(x)::Val{x} can be used for this purpose. The proposed curly brace syntax could only be dubious shorthand for that, not actually omit a type.

2 Likes

Funnily enough it’s possible to abuse the constructor syntax to do pretty much what you describe:

struct f{T} end

f{:x1}() = 1
f{:x2}() = 2

julia> f{:x1}()
1

julia> f{:x2}()
2

This works because constructors seem to be permitted to return any value.

I wonder if it’d make any sense to allow this syntax for any function, not just constructors?

1 Like

So for a given function f you add struct f{T} end
You can then create additional methods of f that you can call specifically

f{“specific_method_1”}( args… )
f{“specific_method_2”}( args… )

The selector has to be hard coded. i.e. this doesn’t work

f{:x}()=1
X = :x
f{X}()

So you could instead just create different functions
f_specific_method_1( args… )
f_specific_method_2( args… )

I like it though. I’d prefer to have one function with different tags rather than different functions. But I’m not an expert.

It doesn’t make sense for the language to turn curly braces into bonus arguments in a hypothetical parametric function call (not to be confused with parametric methods), nor does it make sense to parameterize non-types. Type-based multimethods and parametric types shouldn’t be misappropriated for the syntax of very limited pattern-matching.

This isn’t a function, even if you do struct f{T} <: Function end. Types are callable (the methods being the type constructors), but functions are instances of Function, not subtypes.

Let alone a function, you’re not working with even one type, but an iterated union of infinitely many types. You would not want to nest calls like f{X}() instead of f(X) because of the type instability. Type-based dispatch is only one specific way of branching, it’s not suitable for everything. In this example I’d much prefer indexing a Dict{Symbol, Int}, and in general with a few fixed paths I’d prefer an if-statement or try proper pattern-matching like Match.jl.

It works for me, where does it throw the error and what is it?

You’re right. It does work.

I think the recommended way to do dispatch based on values is using Val like Benny mentioned

f(::Val{:x1}) = 1
f(::Val{:x2}) = 2

f(Val(:x1))

Whether this is the best approach depends on what you’re trying to do, I guess.

Thanks for all your help.
I see now how Val(x) works. I’d missed it before.
So tuukka’s answer above is the best way to dispatch on parameter.
I’ll use this from now on.

Re Match.jl
I love this package but only when the result of each match is one or two lines.
If its 20 lines, multiple methods seems cleaner.