top | item 44896063

(no title)

rybosome | 6 months 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.

discuss

order

mrkeen|6 months ago

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|6 months ago

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?