Hacker News new | ask | show | jobs
by almostdeadguy 1601 days ago
This is notably something haskell has largely been worse at than other languages in the ML family (only recently adding the backpack module system, which appears to be much less capable than the module systems in extended versions of SML and OCaml and is still not supported by tools like stack if I understand it right).

Haskell has modules, but until recently it had no module interfaces, so you could not write your code to depend on an abstract type and associated function definitions that could be swapped out dynamically (for ex: w/ mocks in testing).

Type classes are a related concept (i.e. an abstract definition of functions that may be implemented for a given type), but they enforce additional restrictions like coherency (i.e. only a single instance of class may be implemented for a given type). While this is advantageous in many situations, it's a huge pain in the ass when you need to do something like just mock network IO somewhere (as every combination of things you want to change out is going to need a newtype wrapper + class instances for all effects).

The preferred techniques for handling this the last time I used it were:

1. Transformer classes (usually w/ the "mtl" library + your own custom ones). Basically you could categorize types of useful effects into classes, use those class constraints in your function definitions + push any concrete implementations as high up the stack as possible, and use different monad transformer stacks to swap out the implementations of those effects. There's no getting around the single instance restriction of classes, but this allows you to only change out one "layer" of effects in code (say you have a transformer for network IO, you could change out only that concrete type), which reduces some of the labor involved. But there are subtle implications about the way transformers stack that change the meaning of your code, and the use of functional dependencies in MTL means you can only really use one instance of a class in your stack, so for using MTL to do something like things supply your functions w/ context/config values via MonadReader, you end up needing to smuggle around some unholy god object of everything you'd ever want to inject. Enjoy trying to write legible test cases w/ that.

2. Roll your own classes. This bypasses a lot of the weirdness you run into w/ transformer stacks, but its frankly a pain having to break out every possible effect you'll make use of into classes + defining instances for different use cases. And then you'll run into the reality that many libraries are making use of MTL-style classes so you can't entirely avoid them anyways (though you usually won't need to use MTL class constraints in your own code, and thus sidestep the aforementioned god objects).

3. Free monads. Basically describe your program as functions that don't actually run effects, but instead produce data structures describing a program that can be interpreted in several ways where the effects actually occur. This sounds awful but it's very easy to reason about as you have full control over the evaluation of effects in your interpreter functions, has a lot of nice advantages for testing + debugging as everything that could possibly cause a side effect is now inspectable as data, and is usually the least laborious of all solutions in my experience? This is known to be suboptimal from a performance perspective though.

All that said, backpack seems like it will be an improvement upon all these options, despite not having the full flexibility you get from first-class modules in other languages in the ML family tree.

One other note: 1ML looks like a very cool as a refinement of SML modules [1]. I have no real expertise w/ the design of programming languages to know if there are other problems with this approach, but I'd love to see a production-ready implementation.

[1]: https://people.mpi-sws.org/~rossberg/1ml/1ml-extended.pdf