Hacker News new | ask | show | jobs
by horsawlarway 2050 days ago
Coming in without much OCaml experience, I don't really think this is a great demonstration of why this construct has value.

I don't really want to read a long form description of the OCaml implementation of modules. I want a comparison to the languages he dismissed at the beginning of the article, and a discussion of why this feature has some value that isn't provided by those languages.

Basically - This feels like a different take on generics to me. There might be a lot of value in how this is implemented when compared to generics in a language like Java/C#/Typescript, but I didn't find that content anywhere in the article...

2 comments

Say you'd like to have an interface for things that are 'mappable'. For example, for arrays we could write:

    interface Mappable<Array> {
      map<A, B>(f: (a: A) => B, fa: Array<A>): Array<B>
    }
Likewise, for `Promise`s we could write:

    interface Mappable<Promise> {
      map<A, B>(f: (a: A) => B, fa: Promise<A>): Promise<B>
    }
But in order to generalize this interface to an arbitrary type constructor such that `F: * -> *`, we would need to write

    interface Mappable<F> {
      map<A, B>(f: (a: A) => B, fa: ?): ?
    }
which is not possible in TypeScript since it does not support higher-kinded types or type parameters that take type parameters or parametrized modules.
Not possible, but an approximation:

  interface Mappable<P extends Mappable<P, unknown>, T> {
    flatMap<U>(f: (x: T) => Mappable<P, U>): Mappable<P, U>;
  }

  class Maybe<T> implements Mappable<Maybe<unknown>, T> {
    x: T | undefined;

    public flatMap<U>(f: (x: T) => Maybe<U>): Maybe<U> {
        if (this.x) {
            return f(this.x);
        }
        return Maybe.nothing();
    }
  }
Yes, in fact this reminds me of the HKT implementation[1] found in fp-ts[2][3]

    interface HKT<F, A> {
      _URI: F
      _A: A
    }
    
    interface Mappable<F> {
      map<A, B>(f: (a: A) => B, fa: HKT<F, A>): HKT<F, B>
    }
where F is a unique identifier representing the type constructor and A its type parameter.

[1]: https://www.cl.cam.ac.uk/~jdy22/papers/lightweight-higher-ki... [2]: https://github.com/gcanti/fp-ts [3]: https://gist.github.com/gcanti/2b455c5008c2e1674ab3e8d5790cd...

gcanti’s work in Flow and Typescript is amazing and I use it daily :)
Seems like something covered by typeclasses in Haskell, right?
Seems like it, but typeclasses are inherently anti-modular, they provide a globally coherent unique instance. consider the Ord typeclass giving a single ordering for a specific type. To reverse the order you need to create a new type with a new instance of ord which reverses it.

Where in a module system its perfectly fine to have multiple instances of Ord for a given type. One gives global consistency where the other gives local consistency.

When you put it like that, modules sound more powerful than typeclasses. Is there any situation where typeclasses are superior to modules?
I think the extra power comes with cognitive overhead, I.e. operators are now relative to some module, and you're stuck with that overhead whether or not you actually use the power or not.

Trying to stay neutral, I think its no suprise that some people prefer typeclasses...

In addition to @ratmice's comment, check out this post[1] on Existential Type. I've dabbled in Haskell myself with not much experience in ML, so I found it interesting to see how ML modules differ from Haskell typeclasses. Though they seem to be equi-expressive for the most part.

[1]: https://existentialtype.wordpress.com/2011/04/16/modules-mat...

It's absolutely a different approach to generics. Or, rather, that's the ringer. I want to say first: OCaml's take on modules is just a really nice way of doing namespacing as well.

Secondly, generics depend upon (a) having a means to discuss functionality which abstracts over one or more types and certain behaviors those types must support, (b) having a means to bundle up one or more types along with some behaviors, and (c) being able to combine those two.

In Typescript/Java/C# this is mostly carried out by classes and subtyping. Abstraction occurs when we ask not for a specific type but instead for something a little less than that specific type, one of its supertypes; bundling occurs in classes; and the combination occurs naturally as subtypes are transparently upcast to their supertypes.

There are two practical drawbacks to this approach:

First, it's hard to abstract over behavior that doesn't merely consume your abstract type but also returns it. When we do (c) via subclassing we have to upcast and it's not always clear or possible to re-downcast things back to the appropriate type. OO has tons of workarounds for this issue and related ones.

Second, it's hard to abstract over multiple interrelated types at once. For instance, a generic graph implementation might want to be abstract both in the types of nodes and the type of edges. The generic implementation can thus handle annotations at either the edges or the nodes. In OO abstraction, you might do something like have the edges be an associated type of the nodes, but this creates an unnecessary asymmetry.

The solution is a classic one. Instead of having the class represent an object, have the class represent a bundle of operations which act on abstract objects (the C++ vtable approach). For example, in pseudocode

    class GRAPH

      type Graph
      type Node
      type Edge

      # These are hard to do with subclassing since Graph will often be upcast on return
      def emptyGraph(): Graph
      def simplify(g: Graph): Graph

      # These represent non-trivial interactions between multiple types abstracted simultaneously
      def addNode(g: Graph, n: Node): Graph
      def neighbors(g: Graph, n: Node): List<Node>
And this, with the appropriate type discipline, is what OCaml does. Unfortunately, what you'll find is that OCaml's type discipline is critical and difficult to emulate. Making this sort of modularity work consistently involves some notions of equivalences and transparency that are natural to discuss when talking about modules but rarely show up in OO systems.
All the languages you mention have parameterized types, so I don't see why anyone would be tempted to use subtyping rather than generics. The only reason I could see is wanting to parameterize at runtime, but it's not immediately obvious to me that graphs with runtime parameterized edge and nodes are something you'd want on a regular basis. Am I missing some subtlety?
Parameterized types can help here a lot. I didn't want to speak to them too quickly so I blurred a few lines, but it's a good point.

Parametric types help with part (a) by allowing you to specify only part of the structure of your type. That can help enormously, though they also force some amount of concretion in your type which isn't always good. Ultimately, OCaml's module system is pointed in the direction of ad hoc polymorphism where you pass in behavior with your abstracted types.

Subtyping supports this passing of behavior as it lets you specify a whole space of types abstractly. In that way, it's a little more supportive of the pathway to ad hoc polymorphism.

I love this example. I have a feeling that I'll be borrowing it often in the future.