dep-t-advice
This package is a companion to
dep-t. It provides a mechanism for
handling cross-cutting concerns in your application by adding "advices" to the
functions in your record-of-functions, in a way that is composable and
independent of each function's particular number of arguments.
Rationale
So, you have decided to structure your program in a record-of-functions style,
using dep-t. Good choice!
You have already selected your functions, decided which base monad use for
DepT
, and now you are ready to construct the environment record, which serves
as your composition
root.
Now seems like a good moment to handle some of those pesky "croscutting
concerns", don't you
think?
Stuff like:
- Logging
- Caching
- Monitoring
- Validation
- Setting up transaction boundaries.
- Setting up exception handlers for uncaught exceptions.
But how will you go about it?
A perfectly simple and reasonable solution
Imagine that you want to make this function print its argument to stdout:
foo :: Int -> DepT e IO ()
Easy enough:
foo' :: Int -> DepT e IO ()
foo' arg1 = do
liftIO $ putStrLn (show arg1)
foo arg1
You can even write your own general "printArgs" combinator:
printArgs :: Show a => (a -> DepT e IO ()) -> (a -> DepT e IO ())
printArgs f arg1 = do
liftIO $ putStrLn (show arg1)
f arg1
You could wrap foo
in printArgs
when constructing the record-of-functions,
or perhaps you could modify the corresponding field after the record had been
constructed.
This solution works, and is easy to understand. There's an annoyance though:
you need a different version of printArgs
for each number of arguments a
function might have.
And if you want to compose different combinators (say, printArgs
and
printResult
) before applying them to functions, you need a composition
combinator specific for each number of arguments.
The solution using "advices"
The Advice
datatype provided by this package encapsulates a transformation on
DepT
-effectful functions, in a way that is polymorphic over the number of
arguments. The same advice will work for functions with 0
, 1
or N
arguments.
Advices are parameterized by the constraints they require of the function:
- The function arguments. "All the arguments must be showable".
- The
DepT
environment and the base monad. "The environment must have a
logger, and the base monad must have a MonadIO
instance."
- The function return type. "The function must return a type that is a
Monoid
."
Here's how a printArgs
advice might be defined:
printArgs :: forall e m r. MonadIO m => Handle -> String -> Advice Show e m r
printArgs h prefix =
makeArgsAdvice
( \args -> do
liftIO $ hPutStr h $ prefix ++ ":"
hctraverse_ (Proxy @Show) (\(I a) -> liftIO (hPutStr h (" " ++ show a))) args
liftIO $ hPutStrLn h "\n"
liftIO $ hFlush h
pure args
)
The advice receives the arguments of the function in the form of an n-ary
product
from sop-core. But it
must be polymorphic on the shape of the type-level list which indexes the
product. This makes the advice work for any number of parameters.
The advice would be applied like this:
advise (printArgs stdout "foo args: ") foo
Advices should be applied at the composition root
It's worth emphasizing that advices should be applied at the "composition
root",
the place in our application in which all the disparate functions are assembled
and we commit to a concrete monad, namely DepT
.
Before being brought into the composition root, the functions need not be aware
that DepT
exists. They might be working in some generic MonadReader
environment, plus some constraints on that environment.
Once we decide to use DepT
, we can apply the advice, because advice only
works on functions that end on a DepT
action. Also, advice might depend on
the full gamut of functionality stored in the environment.
Links