We will make a (very) simple banking app.

- It will attempt to withdraw funds
- Logs a message if it fails
- Updates the current balance if it succeeds

withdraw :: ( MonadIO m , MonadLogger m ) => Int -> m (Maybe Int)

withdraw :: ( MonadIO m , MonadLogger m ) => Int -> m (Maybe Int) withdraw desired = do amount <- getCurrentBalance if amount < desired then do log "not enough funds" return Nothing else do putCurrentBalance $ amount - desired return $ Just amount

A new datatype describing if we're running for real:

data Mode = ForReal | Test (IORef Int)

withdraw :: ( MonadIO m , MonadLogger m ) => Mode -> Int -> m (Maybe Int) withdraw mode desired = do amount <- case mode of ForReal -> getCurrentBalance Test ioref -> liftIO $ readIORef ioref if amount < desired then do log "not enough funds" return Nothing else do let putAction = case mode of ForReal -> putCurrentBalance Test ioref -> liftIO . writeIORef ioref putAction $ amount - desired return $ Just amount

- IO is directly exposed
- Test code is interspersed with our real logic
- No compiler guarantees that we mocked
*all*of our IO

... if we could just write the program that we cared about?

class Monad m => MonadBank m where getCurrentBalance :: m Int putCurrentBalance :: Int -> m ()

withdraw :: ( MonadBank m , MonadLogger m ) => Int -> m (Maybe Int) withdraw desired = do amount <- getCurrentBalance if amount < desired then do log "not enough funds" return Nothing else do putCurrentBalance $ amount - desired return $ Just amount

By adding this new constraint, we can abstract over IO.

Our application and test code can swap out different monads.

Or is it?

This abstraction comes with a heavy cost.

newtype IOBankT m a = IOBankT { runIOBankT :: IdentityT m a }

{-# LANGUAGE GeneralizedNewtypeDeriving #-} deriving ( Functor , Applicative , Monad , MonadError e , MonadIO , MonadRWS r w s , MonadReader r , MonadState s , MonadTrans , MonadWriter w , ... )

instance MonadIO m => MonadBank (IOBankT m) where getCurrentBalance = ... putCurrentBalance = ...

instance MonadBank m => MonadBank (ReaderT r m) where getCurrentBalance = lift getCurrentBalance putCurrentBalance = lift . getCurrentBalance instance MonadBank m => MonadBank (WriterT w m) where getCurrentBalance = lift getCurrentBalance putCurrentBalance = lift . getCurrentBalance instance MonadBank m => MonadBank (StateT s m) where getCurrentBalance = lift getCurrentBalance putCurrentBalance = lift . getCurrentBalance -- so many more

Even if they're best practices.

Boilerplate gets in the way.

Everything else we use in Haskell composes.

Why don't monads?

Monadic programs expressed as data structures we can manipulate.

Provided by the freer-effects package.

withdraw :: ( Member Bank r , Member Logger r ) => Int -> Eff r (Maybe Int) withdraw desired = do amount <- getCurrentBalance if amount < desired then do log "not enough funds" return Nothing else do putCurrentBalance $ amount - desired return $ Just amount

withdraw :: ( MonadBank m , MonadLogger m ) => Int -> m (Maybe Int)

➠

withdraw :: ( Member Bank r , Member Logger r ) => Int -> Eff r (Maybe Int)

```
withdraw :: ( Member Bank r
, Member Logger r
)
=> Int
-> Eff r (Maybe Int)
```

```
withdraw :: ( Member Bank r
, Member Logger r
)
=> Int
-> Eff r (Maybe Int)
```

{-# LANGUAGE GADTs #-} data Bank a where GetCurrentBalance :: Bank Int PutCurrentBalance :: Int -> Bank ()

getCurrentBalance :: Member Bank r => Eff r Int getCurrentBalance = send GetCurrentBalance

putCurrentBalance :: Member Bank r => Int -> Eff r () putCurrentBalance amount = send $ PutCurrentBalance amount

{-# LANGUAGE TemplateHaskell #-} data Bank a where GetCurrentBalance :: Bank Int PutCurrentBalance :: Int -> Bank () makeFreer ''Bank

data Logger a where Log :: String -> Logger () makeFreer ''Logger

```
withdraw :: ( Member Bank r
, Member Logger r
)
=> Int
-> Eff r (Maybe Int)
```

> :kind Eff Eff :: [* -> *] -> * -> *

StateT s (ReaderT r IO) a

⬌

Eff '[State s, Reader r, IO] a

main runs in IO -- not in Eff.

We have one special function:

runM :: Monad m => Eff '[m] a -> m a

run :: Eff '[] a -> a

run and runM provide base cases.

We want a function that looks like this:

runLogger :: Eff (Logger ': r) a -> Eff r a

It "peels" a Logger off of our eff stack.

What does it mean to run a Logger?

Maybe we want to log those messages to stdout.

```
runLogger :: Member IO r
=> Eff (Logger ': r) a
-> Eff r a
```

Even though we have IO here, it's not the program that requires it; only the intepretation.

runLogger :: Member IO r => Eff (Logger ': r) a -> Eff r a runLogger = runNat logger2io where logger2io :: Logger x -> IO x logger2io (Log s) = putStrLn s

runBank :: Member IO r => Eff (Bank ': r) a -> Eff r a runBank = runNat bank2io where bank2io :: Bank x -> IO x bank2io GetCurrentBalance = -- use IO to return an Int bank2io (PutCurrentBalance newValue) = -- perform IO and return ()

> :t (runM . runLogger . runBank) Eff '[Bank, Logger, IO] a -> IO a

> :t (runM . runLogger . runBank $ withdraw 50) IO (Maybe Int)

{-# LANGUAGE ScopedTypeVariables #-} ignoreLogger :: forall r a . Eff (Logger ': r) a -> Eff r a ignoreLogger = handleRelay pure bind where bind :: forall x . Logger x -> (x -> Eff r a) -> Eff r a bind (Log _) cont = cont ()

testBank :: forall r a . Int -> Eff (Bank ': r) a -> Eff r a testBank balance = handleRelayS balance (const pure) bind where bind :: forall x . Int -> Bank x -> (Int -> x -> Eff r a) -> Eff r a bind s GetCurrentBalance cont = cont s s bind _ (PutCurrentBalance s') cont = cont s' ()

> :t (run . ignoreLogger . testBank) Eff '[Bank, Logger] a -> a

> :t (run . ignoreLogger . testBank $ withdraw 50) Maybe Int

data Logger a where Log :: String -> Logger ()

data Writer w a where Tell :: w -> Writer w ()

Note: there is no Monoid constraint here!

data Bank a where GetCurrentBalance :: Bank Int PutCurrentBalance :: Int -> Bank ()

data State s a where Get :: State s s Put :: s -> State s ()

withdraw :: ( Member (State Int) r , Member (Writer String) r ) => Int -> Eff r (Maybe Int) withdraw desired = do amount :: Int <- get if amount < desired then do tell "not enough funds" return Nothing else do put $ amount - desired return $ Just amount

More general types are more likely to already have the interpretations that you want.

Yes! But more than just that!

In MTL:

runReaderT :: ReaderT x m a -> x -> m a

In Eff:

runReader :: Eff (Reader x ': r) a -> x -> Eff r a

data Exc e a where ThrowError :: e -> Exc e a makeFreer ''Exc

accumThenThrow :: ( Eq e , Monoid e , Member (Exc e) r ) => Eff (Writer e ': r) a -> Eff r a accumThenThrow prog = do let (a, e) = pureWriter prog unless (e == mempty) $ throwError e return a

data SetOf s a where SetAdd :: s -> SetOf s () SetContains :: s -> SetOf s Bool makeFreer ''SetOf

dedupWriter :: ( Member (SetOf w) r , Member (Writer w) r ) => Eff r a -> Eff r a dedupWriter = interpose pure bind where bind (Tell w) cont = do alreadyTold <- setContains w unless alreadyTold $ do setAdd w tell w cont ()

- Eff gives us the flexibility of MTL without the boilerplate.
- We get testing (mostly) for free.
- We're forced to separate our business logic from implementation details.

Space | Forward |
---|---|

Right, Down, Page Down | Next slide |

Left, Up, Page Up | Previous slide |

P | Open presenter console |

H | Toggle this help |