Remix.run Logo
rybosome 4 days ago

I’d have liked to see the use of dependency injection via the effects system expanded upon. 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.

Also, I had no idea that the module system had its own type system, that’s wild.

mrkeen 4 days ago | parent [-]

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

rybosome 4 days ago | parent [-]

Thanks for the detailed reply, that’s very cool! This looks great, very usable way to do DI.

Do you use Haskell professionally? If so, is this sort of DI style common?

abathologist 4 days ago | parent | next [-]

I am not a Haskell expert, nor an expert in effect systems, but, AFAIU, what mrkeen has provided is an analogous pattern in Haskell -- where effects are represented via the free monad with an interpreter -- and not an account of what is described in the article.

In OCaml we can (and do) also manage effects via monadic style. However, we don't have ad hoc polymorphism (e.g., no typeclasses), so that aspect of the dependency injection must go thru the more verbose (but more explicit, and IMO, easier to reason about) route of parametric modules.

The point in the article is that effect handlers allow direct-style code with custom effects, which enable a kind of "dependency injection" which actually looks and feels much more like just specifying different contexts within which to run the same program. If you are very used to doing everything in Haskell's monadic style, you may not really notice the difference, until you try to compose the handlers.

Here is an example I've put together to show what the author is talking about in practice: https://gist.github.com/shonfeder/a87d7d92626be06d17d2e795c6...

mrkeen 3 days ago | parent | prev [-]

No idea how common it is. It's never been my main work language but I've been using it at work for prototyping/scripting/automation/analysis etc for at least ten years now.

It looks like what I've done is most similar to https://www.parsonsmatt.org/2018/03/22/three_layer_haskell_c...

And I think this was the post that proposed tagless-final over free-monads: https://markkarpov.com/post/free-monad-considered-harmful.ht...