Last post we looked at dealing with state when using our bank account. Here's a recap of the code we ended up with:
-- our bank functions deposit :: Int -> State [Transaction] () deposit amount = modify (\transactions -> transactions ++ [Deposit amount]) withdraw :: Int -> State [Transaction] () withdraw amount = modify (\transactions -> transactions ++ [Withdrawal amount]) getStatement :: State [Transaction] String getStatement = gets generateStatement -- the details of generateStatement are out of scope of this post -- and here's the usage main = do let statement = evalState useMyBank  print statement useMyBank :: State [Transaction] String useMyBank = do deposit 200 withdraw 100 getStatement
Notice how the user has to get the statement and print it to the console, whereas the bank kata states that our library code should have that responsibility.
In order to print to the console, we need to use the
IO monad. Here's a function that prints using
putStr :: String -> IO ()
putStr will take a
String to be printed and return
IO of unit. We want to use this function with the result of
Our first attempt might look something like this:
printStatement :: State [Transaction] () printStatement = do transactions <- get let statement = generateStatement transactions putStr statement
But this won't compile:
-- compiler output simplified for brevity Couldn't match type ‘IO’ with ‘State [Transaction]’ Expected type: State [Transaction] () Actual type: IO ()
putStr we need a type of
IO, but our type is
putStr uses a different type of monad and that doesn't compose with our
To use two or more monads together, you need to use a monad transformer. What does that mean? The simplest definition I've seen is this:
A monad transformer takes something that does one thing, and then adds the capability to do another.
In our case, our current monad does one thing (deals with state), and we want to add the capability to output text to the console.
To do this we will use a monad transformer called
StateT. This has all the functions
State has, plus the ability to use
IO or any other monad too. The
StateT general type is
StateT s m a, where
s is our type of state,
m is the monad capability we want to add, and
a is our return value. As you can see it almost the same type as
State, but with an added
m. With this we can write our
printStatement in almost the same way we specified earlier.
printStatement :: StateT [Transaction] IO () printStatement = do transactions <- get let statement = generateStatement transactions lift (putStr statement)
Notice how the type of
printStatement has changed from
State [Transaction] () to
StateT [Transaction] IO ().
lift. The type of
putStr "a string" is
IO (). This doesn't match the type of
printStatement, we need to make it match. This is what lift does for us.
lift :: m a -> t m a -- more concretely for our use case lift :: IO () -> StateT [Transaction] IO ()
putStr will still work as we expect it to work, but now it's type matches so we can use it within
Users of our code can now tell us to print a statement instead of doing it themselves. To do this there is a
runStateT function, just as there is a
main = do runStateT useMyBank  pure () -- we need to return IO () for main useMyBank :: StateT [Transaction] IO () useMyBank = do deposit 200 withdraw 100 printStatement
Side note: we'll also need to change the type signature of our
withdraw methods to
StateT [Transaction] IO (), but the function implementations don't need to change which is pretty cool.
Uh oh, we've lost our ability to test the statement output, as it is printed as a side effect and not returned.
it "sends statement to the aether" $ do runStateT printStatement [Deposit 100] `shouldBe` ... -- the return type is IO ((), [Transaction]), statement is gone
We need to abstract the printing in some way, as it is at the boundary of our system - just like we would in an OOP language. There are two main options that I know of:
printStatementfunction (Inspired by this blog post).
StateT, which specifies the statement printing behaviour we want. Think of this like an interface in C#/Java.
Simple enough, we will make an inner
printStatement function that takes as a parameter something that prints. We will specify 'something that prints' to be a monad
m (), i.e. something that does a side effect and returns nothing. Notice how we've generalised the type away from
IO (), which means for testing we can specify a different monad which stores the side effect so that we can test the intended output.
printStatement :: StateT [Transaction] IO () printStatement = innerPrintStatement putStr innerPrintStatement :: Monad m => (String -> m ()) -> StateT [Transaction] m () innerPrintStatement printer = do transactions <- get let statement = generateStatement transactions lift (printer statement)
We can now test the innerPrintStatement. Since the
m is polymorphic, we can swap out the
IO for a different monad -
Writer String, which will store our printed statement for us to test.
testPrintStatement :: StateT [Transaction] (Writer String) () testPrintStatement = innerPrintStatement (\statement -> tell statement) it "prints a statement" $ do -- evalStateT works just like evalState, except it will return us a `Writer String ()` instead of just `()` -- we can then use execWriter to get the String from Writer String () execWriter (evalStateT testPrintStatement [Deposit 100, Withdrawal 50]) `shouldBe` "Deposited 100\nWithdrew 50"
That wasn't so bad :) but there are two things I personally don't like.
innerPrintStatementrather than the function actually being used by our users.
Monad m =>is far too generic and doesn't relay the intent of the statement printing code.
Not to worry, from here it's quite easy to refactor to our other solution, which solves these problems.
In haskell, type constraints are used to so that we have access to more functions to deal with our datatypes. As a small example, consider this.
areTheseEqual :: a -> a -> Bool areTheseEqual a b = a == b
Trying to compile this throws an error:
No instance for (Eq a) arising from a use of ‘==’. Our type
a in the signature is as polymorphic as it gets. We know nothing about it, including whether two of that type can be compared for equality.
The answer to this is hinted in the compiler output - we need to specify that
a is an instance of the
Eq class. If we do that we know we will have an
== method available.
Brief explanation aside, let's create a typeclass that represents the intent of printing a statement.
class MonadStatementPrinter m where printStmt :: String -> m ()
Now we can add a type constraint to our
printStatement, such that any
m that is used must have a
printStmt function with the type signature above.
printStatement :: (Monad m, MonadStatementPrinter m) => StateT [Transaction] m () printStatement = do transactions <- get let statement = generateStatement transactions lift $ printStmt statement
Cool. Building this makes the compiler spew an error (we'll talk about the compiler errors in the tests later):
Main.hs:18:3: error: • No instance for (MonadStatementPrinter IO) arising from a use of ‘printStatement’
We have our typeclass constraint (interface), but nothing implementing it! Let's make
IO implement our interface so we can print to the console.
instance MonadStatementPrinter IO where printStmt = putStr
Even cooler. This works without any changes to the usage of our code. What's also good is that though we have our default implementation for IO, our users can also specify their own instance should they need to do something else.
Now for testing. We're getting a similar error to above:
No instance for (MonadStatementPrinter (Writer String)). We just need an instance of
Writer for our
instance MonadStatementPrinter (Writer String) where printStmt = tell
Awesome. Now let's clear up the testing for
withdraw. We don't need our
MonadStatementPrinter constraint for these functions so we can use a simpler monad called
Identity that does nothing, and returns our result.
it "deposits money" $ do runIdentity (execStateT (deposit 100) newBank) `shouldBe` [Deposit 100] it "withdraws money" $ do runIdentity (execStateT (withdraw 100) newBank) `shouldBe` [Withdrawal 100]
Et voila! Our functions are both dealing with state and printing, and are covered by tests.
Software es nuestra pasión.
Somos Software Craftspeople. Construimos software bien elaborado para nuestros clientes, ayudamos a los/as desarrolladores/as a mejorar en su oficio a través de la formación, la orientación y la tutoría. Ayudamos a las empresas a mejorar en la distribución de software.