Bank kata in Haskell — printing a statement

Liam Griffin-Jowett
5 min readFeb 21, 2019

--

Recap

Last post we looked at dealing with state when using our bank account. Here’s a recap of the code we ended up with:

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.

Trying to use state and print

In order to print to the console, we need to use the IO monad. Here’s a function that prints using IO:

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 generateStatement.

Our first attempt might look something like this:

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 ()

To use putStr we need a type of IO, but our type is State [Transaction]! putStr uses a different type of monad and that doesn’t compose with our State.

Monad transformers to the rescue

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.

StateT

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.

Notice how the type of printStatement has changed from State [Transaction] () to StateT [Transaction] IO ().

Let’s explain 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.

putStr will still work as we expect it to work, but now it’s type matches so we can use it within printStatement.

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 runState for State types.

Side note: we’ll also need to change the type signature of our deposit and withdraw methods to StateT [Transaction] IO (), but the function implementations don’t need to change which is pretty cool.

What about testing?

Uh oh, we’ve lost our ability to test the statement output, as it is printed as a side effect and not returned.

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:

1. Abstract what does the printing as a parameter to the printStatement function (Inspired by this blog post).
2. Use a typeclass to specify a type constraint on our m in StateT, which specifies the statement printing behaviour we want. Think of this like an interface in C#/Java.

Abstract printing as a parameter

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.

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.

That wasn’t so bad :) but there are two things I personally don’t like.

  1. We are testing innerPrintStatement rather than the function actually being used by our users.
  2. Our constraint 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.

Use a typeclass constraint

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.

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.

Now we can add a type constraint to our m in printStatement, such that any m that is used must have a printStmt function with the type signature above.

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.

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 MonadStatementPrinter typeclass!

Awesome. Now let’s clear up the testing for deposit and 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.

Et voila! Our functions are both dealing with state and printing, and are covered by tests.

--

--