moo-nad: Invocation helpers for the ReaderT-record-of-functions style.

[ bsd3, control, library ] [ Propose Tags ]

Using a record-of-functions as the environment of some reader-like monad is a common way of structuring Haskell applications, somewhat resembling dependency injection in OOP.

We often want our program logic to be polymorphic over both the concrete monad and the environment. One common solution is to abstract the monad using MonadReader, and abstract the environment using HasX-style typeclasses.

One minor annoyance though is that invoking the function in the environment is often a bit cumbersome: you have to ask the environment for the function, and then lift the result of the function back into the reader-like monad.

This library supports a special twist on ReaderT-record-of-functions style: instead of depending only on typeclasses for abstraction, we also use a module signature. This comes with different tradeoffs.

One benefit is that we support a simpler way of invoking functions from the environment, using a helper that takes care of both asking the environment and lifting function results, and which works uniformly for functions of any arity.

[Skip to Readme]



[Index] [Quick Jump]


Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees


  • No Candidates
Versions [RSS],
Change log
Dependencies base (>= && <5), dep-t (>=0.4.4 && <0.5), moo-nad, mtl (>=2.2 && <2.3), transformers (>=0.5 && <0.6) [details]
License BSD-3-Clause
Author Daniel Diaz Carrete
Category Control
Source repo head: git clone
Uploaded by DanielDiazCarrete at 2021-05-23T13:47:53Z
Downloads 229 total (4 in the last 30 days)
Rating (no votes yet) [estimated by Bayesian average]
Your Rating
  • λ
  • λ
  • λ
Status Docs uploaded by user [build log]
Last success reported on 2021-05-23 [all 1 reports]

Readme for moo-nad-

[back to package description]


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.