Invocation helpers for the ReaderT-record-of-functions style.
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.
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?
See the example-logic-that-logs internal library for an example of abstract program logic that imports an enriched version of
Moo
.See also the example-impl internal library that implements the
Moo
signature.The test suite creates an actual concrete environment and runs the program logic with it.
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.