Hehe, this is a fun subject, but I will try to keep it succinct. ^.^
First of all, OCaml’s modules are what enable full Higher Polymorphic Types. Like Haskell’s Higher Kinded Types (which allow typeclasses and more) HPT’s solve the same problems but in a different way. The parameterized and first-class modules in OCaml are what allow passing strong but ‘unknown’ types ‘through’ functions, which immediately makes obvious that you can emulate HKT’s that way, so it has that ability, BUT instead of requiring whole program type scanning, like Haskell does (and is the major source of its slow compiling), the HPT’s in OCaml only need to know about what is at the point where it is used, thus enabling significantly faster compiling for the same features. With parameterized modules, even ignoring the HPT’ness of their immense capabilities, you can emulate Haskell’s typeclasses. The usual example is to reimplement the Haskell’y show
typeclass, so let me do that here, first lets define the ‘typeclass’ type:
module type Show = sig
type t
val show : t -> string
end
This is just a type of a module, any module that has a type named t
(which is the common ‘module-level’ type name by standard) and has a function in it called show
that takes the t
type of the module and returns a string.
Now let’s define a function that uses this type to get the string of whatever:
let show ( S : Show ) x = S.show x
Now immediately you’d be thinking (if you know Haskell), “Well why not just use a HKT here and pass the types based on ‘x’ directly?!?”, well that is because OCaml is made to be fast, both in compiling and execution, so we need to ‘decorate’ the type. The { S : Show }
is 'destructuring (just like in elixir matching) the passed in First-Class module to give it a name that we can use, just like it was a full module. We then call the show
function that is defined in that module, passing to it x
, which will not compile if x
is a different type from S.t
. So we could do something like this:
print_endline ("Show an int: " ^ show (module struct type t = int; let show x = string_of_int x end : Show) 5)
Here I pass in a module to show, that I define inline (yes you can even define modules inline), that can handle the int of 5
that I pass in, however this is wordy, so as per the common OCaml examples let’s define a few useful global modules that fulfill Show
:
module Show_int : Show = struct
type t = int
let show = string_of_int
end
module Show_float : Show = struct
type t = float
let show = string_of_float
end
module Show_list ( S : Show ) : Show = struct
type t = S.t list
let show = string_of_list S.show
end
Now we can use it like:
print_endline (" Show an int: " ^ show Show_int 5);
print_endline (" Show a float : " ^ show Show_float 1.5);
print_endline (" Show a list of ints : " ^ show Show_list(Show_int) [1; 2; 3]);
Now you can call show
on any type that has an appropriate module defined, and you can pass the module down however deep through many functions. As you can see, Show_list
is a function here, since it will in turn take a module of whatever can display its internal type. This gives you the full power of typeclasses in Haskell, but without the exponential compilation cost (it is O(1) here!), however it does mean having to pass a handler around, what is usually called in OCaml circles as a ‘Witness module’ through the functions. This is where people get that OCaml’s modules can do what typeclasses can (and more) but it is a bit more wordy as you have to carry the witness around.
However, a soon-coming OCaml version has an accepted feature called ‘Implicit Modules’, let me demonstrate:
implicit module Show_int = struct
type t = int
let show = string_of_int
end
implicit module Show_float = struct
type t = float
let show = string_of_float
end
implicit module Show_list { S : Show } = struct
type t = S . t list
let show = string_of_list S.show
end
So, just added the implicit keyword was all (and I do not need to force type them as this will work with any implicit that fulfills the type), in addition to making the argument on the Show_list function take an implicit, an implicit argument is defined with {}
instead of ()
. To use it I just need to redefine my `show function as:
let show { S : Show } x = S.show x
Now S
passed in module is an implicit. I can now use it like:
print_endline (" Show an int: " ^ show 5);
print_endline (" Show a float : " ^ show 1.5);
print_endline (" Show a list of ints : " ^ show [1; 2; 3]);
The modules are looked up by the compiler first by the type the function wants (Show
in this case) and any implicit module that fulfills this is eligible, then it tests the types in the module so that they are compatible, and it fills in at the call site which module is able to be used.
However, this would be a bit magical if done anywhere, and more costly, so to make a module eligible for this it has to be opened implicitly, which you normally open a module via open ModuleName
, to add an implicit module to be able to be used for resolution this is done via open implicit ModuleName
, and doing this could open a whole set of modules as well if ModuleName
module included all the above defined implicit Show modules. This needs to be opened in the scope of where show
will be called, thus at the caller site. If you have implicit functions that call implicit functions and so forth, each implicit function needs to define its implicit parameter, thus only the original base call site require the implicit loaded (and since the call site knows what the type is then that is trivial).
The implicit modules where modeled on Scala’s implicit classes. It allows using Witness modules via a substantially more simple syntax, and functions that use them only define the signature of a module they want to accept, nothing more.
But with all these features, OCaml modules are able to emulate Haskell typeclasses, Haskell HKT’s, and even more, all without compromising the ability for the compiler to optimize or compromising compile time at all. This is why OCaml is one of the fastest compiling native languages out even while doing optimizations that make it rival C++.
F#, for comparison, require .NET, and uses a LOT of dynamic dispatch, which will always make it slower than OCaml itself as the OCaml compiler gets rid of every dynamic dispatch that it can. To do dynamic dispatch in OCaml actually takes more work than just doing things correctly, consequently such code in OCaml is never used unless absolutely necessary. The design of the language encourages doing things the right way.
Also, don’t you love that even using ‘implicit’ things in OCaml still requires an explicit call to state that you are doing so?