Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Other than the syntax, I’m not sure I understand how this is different from any other object-oriented class definition. I suppose the ability to project the module into the local or top-level scope, but that seems more like syntactic sugar than anything meaningful. What am I missing here?


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.


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.


Module signatures may contain types.

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.


Can you give an example of the value this would provide that isn't provided by generics?


One thing the example shows is the classic problem with OO's mechanism, namely binary functions. For a given class, something like `+` is impossible to implement elegantly, it has to bias one operand over the other -- after any syntactic sugar, it's always `arg1.op(arg2)`. Module functors resemble true ADTs more, in that they do not dispatch from a live instance of the datatype.


Module parameterization by type is a kind of a big deal. This enables reuse and composability opportunities that can become pretty impressive.

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.


As another answer pointed out, a class in OOP is supposed to implement only one type and operations on it. It works in many cases, but sometimes it does not fit naturally the problem at hand.

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.


I think Lisp's object system (CLOS) removes this limitation? (Actually a question - I believe this is what multiple dispatch does, but not certain)


I'm not really talking about multiple dispatch, but more like a module that store two or more types, like:

    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.


Except where first-class modules are used, the application of a functor to a module happens at at (or before) compile-time. You can have a fully resolved type before runtime, but which can still be abstract at design time. They can be seen as a zero-cost abstraction.

Modules are structurally typed, so the more typical sub-typing behavior expected from mainstream OOP languages is not applicable to them.


Module functors and first-class modules are probably the big feature. Those are more akin to allowing functions between classes, rather than just objects.


A module, by definition, is a singleton (for parametrized modules, a “once per type” object)

C has (unparametrized) modules, calling them “compilation unit”.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: