Hacker News new | ask | show | jobs
by openasocket 2051 days ago
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.