Bank kata in Haskell - printing a statement

    Liam Griffin-Jowett

    Liam Griffin-Jowett

    See author's bio and posts

    Bank kata in Haskell - printing a statement

    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:

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

    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:

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

    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.

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

    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.

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

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

    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:

    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.

    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.

    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.

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

    instance MonadStatementPrinter (Writer String) where
      printStmt = tell
    

    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.

    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.


    This post was cross-posted to my personal blog.