co-log-0.6.0.0: tutorials/custom/Custom.lhs
# Using a custom monad that stores `LogAction` inside its environment
This tutorial covers the more advanced topic of using the `co-log` library with
a custom application monad.
You can run this tutorial by executing the following command:
```shell
cabal new-run tutorial-custom
```
## Preamble: imports and language extensions
Since this is a literate Haskell file, we need to specify all our language
extensions and imports up front.
```haskell
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE PatternSynonyms #-}
import Prelude hiding (log)
import Colog (pattern D, HasLog (..), pattern I, LogAction, Message, WithLog, log,
richMessageAction)
import Control.Monad.IO.Class (MonadIO)
import Control.Monad.Reader (MonadReader, ReaderT (..))
```
## Application environment
If you have a complex Haskell application, then you are likely to also have
non-trivial settings that configure your application environment. The
environment may store various parameters relevant to your application's
behavior. Interestingly, we can store a `LogAction` inside the same
environment to use it for our logging functions.
The environment for your application may look like this:
```haskell
data Env m = Env
{ envServerPort :: !Int
, envLogAction :: !(LogAction m Message)
}
```
Several notes about this data type:
1. It stores different parameters, like the server port.
2. It stores a `LogAction` that can log values of the `Message` type
from `co-log` in the `m` monad.
3. `Env` is parameterized by type variable `m` which is going to be the
application monad.
The next step is to define an instance of the `HasLog` typeclass for the `Env`
data type. This instance will specify how to get and update the `LogAction`
stored inside the environment.
```haskell
instance HasLog (Env m) Message m where
getLogAction :: Env m -> LogAction m Message
getLogAction = envLogAction
{-# INLINE getLogAction #-}
setLogAction :: LogAction m Message -> Env m -> Env m
setLogAction newLogAction env = env { envLogAction = newLogAction }
{-# INLINE setLogAction #-}
```
That's it! `co-log` requires very little boilerplate.
## Application monad
Now let's define our application monad:
```haskell
newtype App a = App
{ unApp :: ReaderT (Env App) IO a
} deriving newtype (Functor, Applicative, Monad, MonadIO, MonadReader (Env App))
```
This monad stores `Env` parameterized by the monad itself in its context.
Nothing special is required here to tell the monad how to use the logger.
## Example
`co-log` relies on the tagless final technique for writing functions. So you
define your monadic actions with the `WithLog` constraint that allows you to
perform logging:
```haskell
example :: WithLog env Message m => m ()
example = do
log D "First message..."
log I "Second message..."
```
The `WithLog` constraint has three type parameters: the application environment,
the type of the message and the monad. Function `log` takes two parameters:
the logger severity and the log message's text.
## Running example
Now we are ready to execute this action.
First, let's create an example environment:
```haskell
simpleEnv :: Env App
simpleEnv = Env
{ envServerPort = 8081
, envLogAction = richMessageAction
}
```
Then we need to define a function that performs actions of type `App`:
```haskell
runApp :: Env App -> App a -> IO a
runApp env app = runReaderT (unApp app) env
```
Putting it all together, we can specialize the `WithLog` constraint to our
`App` monad and run our example.
```haskell
main :: IO ()
main = runApp simpleEnv example
```
And the output will look like this:
