zoran119
zoran119

Reputation: 11307

Using Maybe and Writer together

Here is my egg packing factory:

type Eggs = Int
data Carton = Carton Eggs deriving Show

add :: Eggs -> Carton -> Maybe Carton
add e (Carton c)
    | c + e <= 12 = Just (Carton $ c + e)
    | otherwise = Nothing

main = do
    print $ pure(Carton 2) >>= add 4 >>= add 4 >>= add 3

Seems to work well, I can nicely chain add functions.

But I want to record a log of how many eggs were added at every step. So I do this:

import Control.Monad.Writer

type Eggs = Int
data Carton = Carton Eggs deriving Show

add :: Eggs -> Carton -> Writer [String] (Maybe Carton)
add e (Carton c)
    | c + e <= 12 = do
        tell ["adding " ++ show e]
        return (Just (Carton $ c + e))
    | otherwise = do
        tell ["cannot add " ++ show e]
        return Nothing

main = do
    let c = add 4 $ Carton 2
    print $ fst $ runWriter c
    mapM_ putStrLn $ snd $ runWriter c

This gives me what I want: I can see the resulting carton and the record for 4 eggs being added.

But I seem to have lost the ability to chain add functions like I did before:

let c = pure(Carton 2) >>= add 4 -- works
let c = pure(Carton 2) >>= add 4 >>= add 2 -- does not work

How can I chain my new writer-enabled add functions? Is there a better way of doing this?

Upvotes: 5

Views: 376

Answers (3)

mariop
mariop

Reputation: 3216

In the first example, the second >>= in the expression is for the Monad instance of Maybe while in the second example it is from the Monad instance of Writer. Specifically, in the first example the >>= expects a function with type Carton -> Maybe Carton, like add 2, while in the second example >>= expects a function of type Maybe Carton -> Writer [String] (Maybe Carton). In both examples pure (Carton 2)>>=add 4 works because pure (Carton 2) has type Maybe Carton and add 4 has type Carton -> <something>, so you have no problems. Adding another >>= to the expression triggers the error because in the first example this >>= has the same type of the first one while in the second example it isn't the same >>=. A solution can be to change add such that it has type Eggs -> Maybe Carton -> Writer [String] (Maybe Carton):

add :: Eggs -> Maybe Carton -> Writer [String] (Maybe Carton)
add e Nothing = return Nothing
add e (Just (Carton c))
    | c + e <= 12 = do
        tell ["adding " ++ show e]
        return (Just (Carton $ c + e))
    | otherwise = do
        tell ["cannot add " ++ show e]
        return Nothing

note that this means that you cannot use anymore pure (Carton 2) but you need pure (Just $ Carton 2):

 > pure (Just $ Carton 2) >>= add 2 >>= add 5
 WriterT (Identity (Just (Carton 9),["adding 2","adding 5"]))

Said that, I would suggest you to use monad transformers to compose Maybe and Writer because this in a common use case for them. Your example could be rewritten as

import Control.Monad.Trans.Maybe
import Control.Monad.Writer

type Eggs = Int
data Carton = Carton Eggs deriving Show

add :: Eggs -> Carton -> MaybeT (Writer [String]) Carton
add e (Carton c)
    | c + e <= 12 = do
        lift $ tell ["adding " ++ show e]
        return (Carton $ c + e)
    | otherwise = do
        lift $ tell ["cannot add " ++ show e]
        mzero

main = do
    let c = return (Carton 2) >>= add 4 >>= add 2
    let result = runWriter $ runMaybeT c
    print $ fst $ result
    mapM_ putStrLn $ snd $ result

A few things have changed from your example:

  1. MaybeT m a is the monad transformer. In this example, m is Writer [String] and a is Carton. To run everything we first runMaybeT, which gives you a Writer [String] (Maybe Carton), and then we call runWriter on it like you have done in your example.
  2. To use Writer functions in MaybeT (Writer [String]) we need to lift them. For instance lift $ tell ["something"]
  3. return carton is used to return a Just Carton while mzero is used to return Nothing

One last thing: in this example we cannot compose Maybe and Writer the other way around, with WriterT [String] Maybe Carton, because when there are more than 12 eggs runWriterT would return Nothing and suppress the history:

import Control.Monad
import Control.Monad.Trans
import Control.Applicative
import Control.Monad.Trans.Maybe
import Control.Monad.Trans.Writer

type Eggs = Int
data Carton = Carton Eggs deriving Show

add :: Eggs -> Carton -> WriterT [String] Maybe Carton
add e (Carton c)
    | c + e <= 12 = do
        tell ["adding " ++ show e]
        lift $ Just $ Carton $ c + e
    | otherwise = do
        tell ["cannot add " ++ show e]
        lift Nothing

main = do
    let c = return (Carton 2) >>= add 4 >>= add 20
    case runWriterT c of
      Nothing ->
        print "nothing to print"
      Just (carton, history) -> do
        print carton
        mapM_ putStrLn $ history

Upvotes: 2

Cactus
Cactus

Reputation: 27626

I'd change add &c to use MaybeT (Writer [String]):

import Control.Monad.Writer
import Control.Monad.Trans.Maybe

type Eggs = Int
data Carton = Carton Eggs deriving Show

main = do
    let c = add 4 $ Carton 2
        (result, log) = runWriter $ runMaybeT c
    print result
    mapM_ putStrLn log

add :: Eggs -> Carton -> MaybeT (Writer [String]) Carton
add e (Carton c)
    | c + e <= 12 = do
          tell ["adding " ++ show e]
          return $ Carton $ c + e
    | otherwise = do
          tell ["cannot add " ++ show e]
          mzero

this will allow your original code of

pure (Carton 2) >>= add 4 >>= add 2

to work as expected.

Upvotes: 5

ErikR
ErikR

Reputation: 52029

Just compose add with MaybeT:

import Control.Trans.Monad.Maybe

test = pure (Carton 2) >>= MaybeT . add 3
                       >>= MaybeT . add 4
                       >>= MaybeT . add 5

runTest = do
  print $ fst $ runWriter (runMaybeT test)

Full example at: http://lpaste.net/169070

Upvotes: 6

Related Questions