It’s even simpler than that, a golang interface is just a simple dictionary of closures. It’s similar to the solution given by @jobjo to the shape design problem. What I did in this thread, or what do RWO, with first-class modules is to make explicit the structure of these closures.
Look at the Golang example for their interface system (interface in golang). When you want to implement an interface for a given type, you just have to define a closure that captures a value of that type:
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
in this case Abs
is a closure that encapsulates a MyFloat
named f
. And the system will automatically instantiates the right dictionary when you convert a MyFloat
to an Abser
(this is the implicit part of modular implicits, that you also find with Haskell’s type classes or Rust’s traits).
They define their interface this way:
type Abser interface {
Abs() float64
}
In OCaml, we will use this dictionary of closures:
type abser = { abs : unit -> float }
but, since this type will be populated with closures, its values will be be partial applications of closed functions of this following type (I just remove the unit
to simplify):
type 'a abser_closed : { abs : 'a -> float }
And now, to complete the closure, we need a partial application and hence a value to apply the closed function. So the original abser
type is near to this one:
type 'a abser_closure = 'a abser_closed * 'a
And since the type of the value encapsulated in the closure is unknown to the user, we need to wrap this in an existential:
type abser = Abser : 'a abser_closure
We could also have use first-class modules, since the parametric dictionary 'a abser_closed
is equivalent to this one:
module type Abser = sig
type t
val abs : t -> float
end
type 'a abser_closed_with_module = (module Abser with type t = 'a)
The interest of first-class module is that we can divide the code in clear and separate compilation units, and then create easily a closures dictionary from them as illustrated in the chapter about first-class module in RWO.
By the way, that’s also how works ad hoc polymorphism in OOP : an object is just a dictionary of closures that shared the same environment (the instance variables).
Nevertheless, with simple closures, you will end up with problem as soon as you do not only consume your value but you also produce them, like in this interface:
module type Abser_extended = sig
include Abser
val double : t -> t
end
Here, you can still use the solution with first-class module and existential GADT. But, as soon as you want to add binary operator (for instance overloading the addition) you’re stuck with this solution and you need to rely on the highest solution for ad hoc polymorphism in OCaml: functor or first-class functor (but without existential GADT).
Geometrically, the solution with GADT can be seen with this cone:
the circular base of the cone represent all the types of the language that can implement the abser
interface (each point is a type) and the top or apex A of the cone is precisely the type abser
. You can see it as a directed graph where each edge (that goes from the base to the apex) is labelled with the dictionary (the first-class module) that shows how a type can be viewed as an abser
. Now, when you want to use an asber
value, you have to reverse the direction of the graph: the edges go from the apex to the circular base, and the purpose of such a value is to dispatch the right dictionary to the right type. I guess that the dispatching mechanism at the heart of some form of ad hoc polymorphism can be well seen with the shape of the cone.
To conclude, there is something that we don’t see with the cone: in the two previous examples (Abser
and Abser_extended
) the apex also belongs to the base circle (an abser
value also satisfies the asber
interface), but it’s not the case when we have binary operators and that’s why we need the full power of functors (the implicit part is just here to reduce boilerplate).