Hacker News new | ask | show | jobs
by kdkeyser 2050 days ago
For me, the value of a module is that you can describe a set of multiple types and the functions that operate on these types, all in one place. In OO, there is essentially one type that is "special", the type of the class you define.

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.

1 comments

I'm trying to wrap my head around this

> 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) { ... }
    }
It's not exactly the same thing because, for example, there is no subtyping or inheritance. In Ocaml you don't say that "type T1 is an Orderable".

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.

This makes it seem like modules are strictly inferior, if you can't assign names to common requirements.
I don't see how you come to that conclusion. Assigning a name to a requirement (~ an interface) is the most simple usage of the module. E.g. to define something like IComparable, you could write:

  module type Comparable = sig
    type t
    val compare : t -> t -> int
  end
You can still give a name to the interface. However, interfaces apply to modules, not to types. In Ocaml you'd say "this module implements the Comparable interface" while in an OO languague you'd say "this type is a subtype of Comparable". Sorry for the confusion.
Ah, thanks for the clarification.
In your example, there is no relation between anything in Module1Interface and Module2Interface.

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)
Yes and no, because a module can be a much more complicated thing than a class. A module allows you to define not just one type but several types and how they interact. In regular OOP you can have a "FooInterface<A, B, C>" where you are defining a type of object, and defining it's behaviors in the context of types A, B, and C. A module defining only one type is pretty much an interface but when you define a module in terms of multiple types it takes on a different shape. While you can probably always replace a module with a series of interfaces (ignoring the part about constructors), those interfaces will be more unwieldy and awkward.

Here'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.