The best typeclass/multiple dispatch implementation I’ve seen was in Coq. It’s also (conceptually) the simplest one, which is not surprising, because in a dependently-typed language one can treat instances and types as regular variables. So, in a sense, it unveils a lot of the magic which I spoke against in the previous thread. Roughly speaking, Coq’s approach to the “protocols” boils down to the following mechanisms:
- implicit arguments (arguments of a function that can be inferred from the context, and therefore can be omitted by the caller)
- type inference for said arguments
- implicit generalization mechanism that replaces free variables in a statement with values automatically inferred from the context according to some rules
The problem, of course, is that Erlang doesn’t have user types. But if you look closely, this is very reminiscent of passing a callback module as an argument to an Erlang function. If one substitutes proper types for module atoms, I can speculate that it can be reproduced without affecting the bytecode and/or VM, or maybe even can be slapped together with a parse transform.
So, how can it work? Start from normal Erlang code:
foo(PrettyPrintModule, A) ->
pretty_printer:pretty_print(PrettyPrintModule, A).
In Coq terms, with a lot of leeway and imagination, PrinterCallbackModule would be an “instance” of “PrettyPrint” “class”.
Step 1: Let’s “improve” this function by emulating an implicit variable. This can be done by defining a new function foo/1
that tries to guess the first argument:
foo(A) ->
PrettyPrintModule = multiple_dispatch:dependent_module(pretty_printer, A),
foo(PrettyPrintModule, A).
Note, that this function can be generated mechanically. I could easily write a parse transform that takes an annotation
-implicit(foo/2, {PrettyPrintModule, pretty_printer}).
foo(PrettyPrintModule, A) ->
...
…and transforms it to foo/1
.
How does multiple_dispatch:dependent_module
work? I guess it can assume that the second argument is a tuple where the first argument uniquely identifies the type. Then it can lookup VMT stored in a persistent term or something.
Step 2: Let’s “improve” the implementation of foo/2
by emulating implicit generalization (again, annotations and parse transforms should do the trick). foo/2
becomes:
-implicit(foo/2, {PrettyPrintModule, pretty_printer}).
foo(PrettyPrintModule, A) ->
pretty_print(A).
and it can be invoked as
foo(MyDatatype)
Here, the compiler should know that pretty_print
is a “method” of pretty_print
, but nothing parse transforms can’t handle.
So, that’s how we get the best typeclasses on the market, lifted straight from Coq.
Step 3: Don’t do that.