dep-t-advice: Giving good advice to functions in a DepT environment.

[ bsd3, control, library ] [ Propose Tags ]

Companion to the dep-t package. Easily add behaviour to functions living in a DepT environment, whatever the number of arguments they might have.

In other words: something like the "advices" of aspect-oriented programming.


[Skip to Readme]

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees

Candidates

  • No Candidates
Versions [RSS] 0.1.0.0, 0.2.0.0, 0.2.0.1, 0.3.0.0, 0.4.0.0, 0.4.0.1, 0.4.4.0, 0.4.5.0, 0.4.6.0, 0.4.6.1, 0.4.7.0, 0.5.0.0, 0.5.1.0, 0.6.0.0, 0.6.1.0, 0.6.2.0
Change log CHANGELOG.md
Dependencies base (>=4.10.0.0 && <5), dep-t (>=0.4.0.0 && <0.5), sop-core (>=0.5.0.0 && <0.6), transformers (>=0.5.0.0 && <0.6) [details]
License BSD-3-Clause
Author Daniel Diaz
Maintainer diaz_carrete@yahoo.com
Category Control
Source repo head: git clone https://github.com/danidiaz/dep-t-advice.git
Uploaded by DanielDiazCarrete at 2021-02-08T10:58:22Z
Distributions
Downloads 2269 total (36 in the last 30 days)
Rating (no votes yet) [estimated by Bayesian average]
Your Rating
  • λ
  • λ
  • λ
Status Docs available [build log]
Last success reported on 2021-02-08 [all 1 reports]

Readme for dep-t-advice-0.4.4.0

[back to package description]

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 can't change the type of a function, but they might:

  • Analyze and change the values of the function's arguments.

  • Add additional effects to the function, either effects from the base monad, or effects from handlers found in the environment.

  • Change the result value of the function.

  • Sidestep the execution of the function altogether, providing al alternative result.

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.