@hackage dep-t-advice0.2.0.0

Giving good advice to functions in a DepT environment.

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.