by mrkeen | 2025-08-14 11:12:01+00:00
Haskeller here!

> The idea that the example program could use pattern matching to bind to either test values or production ones is interesting, but I can’t conceptualize what that would look like with the verbal description alone.

The article appears to have described the free monad + interpreter pattern, that is, each business-logic statement doesn't execute the action (as a verb), but instead constructs it as a noun and slots it into some kind of AST. Once you have an AST you can execute it with either a ProdAstVisitor or a TestAstVisitor which will carry out the commands for real.

More specific to your question, it sounds like the pattern matching you mentioned is choosing between Test.ReadFile and Test.WriteFile at each node of the AST (not between Test.ReadFile and Prod.ReadFile.)

I think the Haskell community turned away a little from free monad + interpreter when it was pointed out that the 'tagless final' approach does the same thing with less ceremory, by just using typeclasses.

> I’d have liked to see the use of dependency injection via the effects system expanded upon.

I'm currently doing DI via effects, and I found a technique I'm super happy with:

At the lowest level, I have a bunch of classes & functions which I call capabilities, e.g

FileOps (readTextFile, writeTextFile, ...) Logger (info, warn, err, ...) Restful (postJsonBody, ...)

These are tightly-focused on doing one thing, and must not know anything about the business. No code here would need to change if I changed jobs.

At the next level up I have classes & functions which can know about the business (and the lower level capabilities)

StoredCommands (fetchStoredCommands) - this uses the 'Restful' capability above to construct and send a payload to our business servers.

At the top of my stack I have a type called CliApp, which represents all the business logic things I can do, e.g.

I associate CliApp to all its actual implementations (low-level and mid-level) using type classes:

instance FileOps CliApp where readTextFile = readTextFileImpl writeTextFile = writeTextFileImpl ... instance Logger CliApp where info = infoImpl warn = warnImpl err = errImpl ... instance StoredCommands CliApp where fetchStoredCommands = fetchStoredCommandsImpl ...

In this way, CliApp doesn't have any of 'its own' implementations, it's just a set of bindings to the actual implementations.

I can create a CliTestApp which has a different set of bindings, e.g.

instance Logger CliTestApp where info msg = -- maybe store message using in-memory list so I can assert on it?

Now here's where it gets interesting. Each function (all the way from top to bottom) has its effects explicitly in the type system. If you're unfamiliar with Haskell, a function either having IO or not (in its type sig) is a big deal. Non-IO essentially rules out non-determinism.

The low-level prod code (capabilites) are allowed to do IO, as signaled by the MonadIO in the type sig:

readTextFileImpl :: MonadIO m => FilePath -> m (Either String Text)

but the equivalent test double is not allowed to do IO, per:

readTextFileTest :: Monad m => FilePath -> m (Either String Text)

And where it gets crazy for me is: the high-level business logic (e.g. fetchStoredCommands) will be allowed to do IO if run via CliApp, but will not be allowed to do IO if run via CliTestApp, which for me is 'having my cake and eating it too'.

Another way of looking at it is, if I invent a new capability (e.g. Caching) and start calling it from my business logic, the CliTestApp pointing at that same business logic will compile-time error that it doesn't have its own Caching implementation. If I try to 'cheat' by wiring the CliTestApp to the prod Caching (which would make my test cases non-deterministic) I'll get another compile-time error.

Would it work in OCaml? Not sure, the article says:

> Currently, it should be noted that effect propagation is not tracked by the type system