The power of the Ocaml module system is really in the functor, which the article only touches upon. You define a module and refer in its definition to types/functions of other modules that must be provided at the time you construct the module. Even better, you can define relations between the types and do type substitutions, e.g.: to construct this module, you need to give me 2 other modules, each with a specific set of functions, and for which type t1 of the first module, matches type t2 of the second module, while the actual type of t1/t2 does not matter.
See https://dev.realworldocaml.org/functors.html for more examples.
> Even better, you can define relations between the types and do type substitutions, e.g.: to construct this module, you need to give me 2 other modules, each with a specific set of functions, and for which type t1 of the first module, matches type t2 of the second module, while the actual type of t1/t2 does not matter.
Isn't this essentially the same as generic type arguments in other languages? Like in this pseudo TypeScript:
class CustomModule<T1 extends Module1Interface, T2 extends Module2Interface> {
constructor(t1: T1, t2: T2) { ... }
}However, it does serve a similar purpose. For example, if you want to create a datatype for an OrderedList then you'd create a higher-order-module (functor) that receives as an argument another module containing all the necessary comparison functions for the list elements. For example, if you apply the OrderedList functor to the IntOrdering module the result would be an OrderedListOfInt module that provides an abstract data type implementing an ordered list of integers.
In the Ocaml standard library the names would be different but that's the basic idea.
module type Comparable = sig
type t
val compare : t -> t -> int
endHere's an example. This example is a bit contrived, because I couldn't think of a better example that was simple and yet demonstrated the power of modules. So this example can be re-phrased and re-structured into a more natural OO fit, but try to look past that. Suppose you want to make a module or set of interfaces that describe a classic board game (i.e. Chess or Checkers). So you have a Board. A Board has a series of Pieces, and each Piece has a set of valid moves on the board. And a move can be applied to a Board to modify the state of the game. Again, very much glossing over the details here to get to the meat of it. So you could write a series of interfaces
interface Board {
List<Piece> getPieces();
}
interface Piece {
List<Move> getValidMoves(b: Board);
}
interface Move {
void apply(b: Board);
}
But on it's own that isn't enough, because you don't want to be able to mix-and-match different interfaces for different games, like trying to find the set of valid moves for a chess piece on a checker board. So you need to apply generics. interface Board<P> {
List<P> getPieces();
}
interface Piece<B, M> {
List<M> getValidMoves(b: B);
}
interface Move<B> {
void apply(b: B);
}
Only that's not enough either, because on it's own these parametric definitions don't enforce that the set of pieces a board returns are actually valid pieces for that game. With constraints you end up with this (in a psuedo-language where you can use a special Self type, I don't know typescript and don't think this can actually be implemented in plain Java) interface Board<P extends Piece<Self,Move<Self>>> {
List<P> getPieces();
}
interface Piece<B extends Board<Self,M>, M extends Move<B>> {
List<M> getValidMoves(b: B);
}
interface Move<B extends Board<?>> {
void apply(b: B);
}
And so you have a bunch of these weird circular definitions to get these components to play together nicely. Meanwhile, you can define a module for this without using Functors or type substitutions or anything terribly complicated (Note the below is sort of mixing OCaml with more Java-like syntax just because I'm not super familiar with OCaml): module type Game = sig
type Board
type Piece
type Move
val getPieces: Board -> List<Piece>
val getValidMoves: Piece -> Board -> List<Move>
val apply: Move -> Board -> unit
end
And I think that's the really interesting thing you can do with modules that is more awkward with the traditional OOP interfaces. It makes it more natural to talk about multiple different data types all working together.The only way to implement that as nicely with an OOP interface would be to wrap everything in a top-level object
interface Game<B, P, M> {
List<P> getPieces(b: B);
List<M> getMoves(p: P, b: B);
void apply(m: M, b: B);
}
Though now you have to make a singleton Game object and pass that around everywhere you need it, which may or may not be idiomatic or obvious depending on the language and your preferences.Probably closer would be (not sure if this is possible in Typescript):
class CustomModule<S, T1 extends Module1Interface<S>, T2 extends Module2Interface<S> > {
constructor(t1: T1, t2: T2) { ... }
}
Meaning that, for example, within Module1Inteface, there is some function f1 that returns an S, and within Module2Interface, there is some function f2 that takes an S as argument.This does become a bit tedious notation-wise, if possible at all. In Ocaml, this would look like:
module CustomModule(M1: Module1)(M2: Module2 with type s = M1.s)So you can write a signature like:
module type VectorSpace = sig
module Field : Field
type t
val zero : t
val (+) : t * t -> t
val (*) : Field.t * t -> t
end
In OOP it becomes hard to write an interface like this simple example, and more complex examples become harder still.C++ templates can be used in a similar way, but it's not perfect. The closest match I can think of is a template class with static members.
D has one more mechanism that is conceptually very different, but could be used to express somewhat similar patterns: template mixins. These are best described as parameterized template sections of code worh one or more declarations that have to be reasonably well formed on their own and are virtually pasted into the code at the location where they are instantiated. They exist somewhere in the space between templates and C preprocessor macros.
Modules relax this single-type requirement, and let you define multiple types in it, which makes certain things more natural to express, without a need to create multiple shallow classes or manager-like classes.
Also, functors (i.e. "module functions") in the OCaml module system allows generics, when a module is essentially parameterized by one or more other modules.
module Data = struct
type pair = int * float
type numbers = int list
let make_pair () = (0, 0.0)
let make_numbers () = []
end
This example is not very illustrative, but just explain the idea what I mean. We have two constructors: make_pair and make_numbers. So in a way, we can have multiple types in the module, if they are meant to be tightly related. We are not forced to make three classes here (Pair, Numbers, Data), everything is in one module.EDIT: OOP classes have a primary type in your class, and sometimes this creates artificial chicken or egg type of problem, where you cannot decide what is more fundamental (message vs receiver vs sender). Modules don't force this on you.
Modules are structurally typed, so the more typical sub-typing behavior expected from mainstream OOP languages is not applicable to them.
C has (unparametrized) modules, calling them “compilation unit”.