@hackage moo-nad0.1.0.2

Invocation helpers for the ReaderT-record-of-functions style.

  • Installation

  • Dependencies (11)

  • Dependents (0)

moo-nad

In this Stack Overflow question, I asked how to simplify the invocation of functions stored in a ReaderT environment.

For example, when invoking a Int -> String -> _ () logging function from the environment, I would like to simply be able to write:

logic :: ReaderT EnvWithLogger IO ()
logic = do
    self logger 7 "this is a message"

instead of something like

logic :: ReaderT EnvWithLogger IO ()
logic = do
    e <- ask
    liftIO $ logger e 7 "this is a message"

(Yes, I'm aware that this isn't that big of a hassle, and that solving it might overcomplicate other things. But bear with me.)

The question received this answer, which worked like a charm. The answer also included the following comment:

Implementing variadics with type classes is generally frowned upon because of how fragile they are, but it works well here because the RIO type provides a natural base case

That got me thinking: is there a way to avoid tying the workings of the helper typeclass to a concrete monad, like RIO? Can the call-helper code be made to work with a variety of reader-like monads?

After a number of failed attempts using a typeclass-only approach, I turned to the solution explored in the current repo: abstract the monad and the environment using a module signature.

That signature is called Moo, and the module Moo.Prelude provides the self and call helper methods.

How to use this library to write program logic that is polymorphic on the monad and the environment?

This is an alternative to the usual way of abstracting the monad using mtl.

Put program logic into indefinite libraries which depend on the Moo module signature. Import Moo.Prelude for the call helpers.

You'll likely need to expand the base Moo signature through signature merging to require extra capabilities from the monad and/or the environment.

(Note: this approach is less fine-grained with respect to constraints than the MTL one. When using MTL each individual function can have different constraints. But here, functions from modules that import the same version of Moo will share the same constraints. If you want constraint differentiation, you'll need to create separate compilation units with different "enriched" versions of Moo.)

You'll eventually need to write an implementation library that gives concrete instantiations for the monad and the environment.

In your executable, depend on both your program logic and the implementation library. The magic of mixing matching will take place, and you'll end up with a concrete version of your logic.

Very well; how does an actual example look like?

Because we are using Backpack, we need to look at how everything is wired together in the cabal file. Notice in particular how:

  • The program logic depends on moo-nad but not on the implementation.

  • The implementation doesn't depend on moo-nad. Implementations in Backpack don't depend on the signatures they implement.

  • The test suite depends on the program logic and the implementation.

caveat emptor

At the end of the day, this method might involve too much ceremony to be practical.

Feedback welcome.