These days, the functions map
, filter
and fold
/reduce
are pretty well
known by most programmers. Any pure data manipulation performed in a loop can
be re-written with a fold
, but it’s not always easy to understand. We use
map
and filter
when we can to make things clearer, and fold
is useful
when folding a collection down to a single value. However, for more complex
loops, most would argue that an imperative loop is easier to understand than
trying to wrap up all the state in the accumulator of a fold. In this
article, we look at a situation where using fold
to map over a collection
while maintaining state can be clarified by mapping over a State
monad.
The Challenge
The map
function lets us transform each element in a list, like this:
newList = map (+1) [1, 2, 3]
-- newList is [2, 3, 4]
But what if we want to do something like this (Javascript):
function addAnIncrementingAmount(numbers) {
let result = [];
let amountToAdd = 0;
for (const number of numbers) {
result.push(number + amountToAdd);
amountToAdd++;
}
return result;
}
const newList = addAnIncrementingAmount([1, 2, 3]);
// newList is [1, 3, 5]
Even though the result is a list mapping each element from an element
provided in the argument, we are unable to use map
because the calculation
for each element is dependant on the previous one. Therefore, we have to use
foldl
instead:
addAnIncrementingAmount :: [Int] -> [Int]
addAnIncrementingAmount numbers =
fst $ foldl addAmount addAmount ([], 0) numbers
addAmount :: ([Int], Int) -> Int -> ([Int], Int)
addAmount (result, amountToAdd) number =
(result ++ [number + amountToAdd], amountToAdd + 1)
While this code is fairly simple (because the example itself is very simple),
if we had more stateful values used in the calculation then it would get more
complex. Also, the addAmount
is trying to do two things at once - i.e. build
the resulting list and perform the calculations.
Generalising with a new higher-order function
Let’s start by separating the calculation from the stateful mapping. We can do
this by creating a new mapWithState
higher-order function.
addAnIncrementingAmount :: [Int] -> [Int]
addAnIncrementingAmount numbers = mapWithState addAmount 0 numbers
mapWithState :: (a -> state -> (b, state)) -> state -> [a] -> [b]
mapWithState f state xs = fst $ foldl f ([], state) xs
addAmount :: Int -> Int -> (Int, Int)
addAmount number amountToAdd = (number + amountToAdd, amountToAdd + 1)
This is a nice separation of concerns. The mapWithState
function is concerned
with building the resulting list and passing the state through, while the
addAmount
function is only concerned with our calculation. This is a good
start, but it’s not clear in the type of addAmount
which argument is the
state and which is the element we are mapping - can we do something about
that?
The type signature should give us a clue as to what we could try. The fact that
it returns a tuple of (result, newState)
should make us think about the
State
monad.
The State
Monad
State
is available via Control.Monad.State
of the mtl
1 package. It is
defined like this:
newtype State s a = State { runState :: State s a -> s -> a }
And is used like this:
import Control.Monad.State (State, get, modify, put, evalState)
performCalculation :: Int -> Int
performCalculation n = evalState statefulCalculation n
statefulCalculation :: State Int Int
statefulCalculation = do
initalValue <- get
put (initialValue + 3)
modify (*2)
newValue <- get
return (newValue - initalValue)
Which would be be the equivilent to this in Javascript:
function performCalculation(initalValue) {
let newValue = initalValue + 3
newValue = newValue * 2
return (newValue - initalValue)
}
While the example above isn’t particularly beautiful, I hope it provides enough
information about using the State
monad for our example. One additional point
about State
(or any other monad) is that its context is maintained through
functional calls within the monad. Let’s see that:
import Control.Monad.State (State, get, modify, put, evalState)
performCalculation :: Int -> Int
performCalculation n = evalState statefulCalculation n
statefulCalculation :: State Int Int
statefulCalculation = do
initalValue <- get
add 3
multiply 2
newValue <- get
return (newValue - initalValue)
add :: Int -> State Int ()
add amount = modify (+amount)
multiply :: Int -> State Int ()
multiply amount = modify (*amount)
Back to our example
Now we know about the State
monad, let’s try re-writing the addAmount
function to use it:
addAmount :: Int -> State Int Int
addAmount number = do
amountToAdd <- get
modify (+1)
return (number + amountToAdd)
Now the type tells us something interesting about the function - it takes an Int and returns an Int, but it also maintains some state (also of type Int). But how do we use it?
Traversable
We know that we want to our addAmount
function map
over our input list, so
what do we get if we try that now?
Prelude Control.Monad.State> :t map addAmount [1, 2, 3]
map addAmount [1, 2, 3] :: [State Int Int]
We’ve individually added each element into a State
and ended up with [State
Int Int]
, but that’s not what we need, we want to compute the entire list
inside State
- i.e. State Int [Int]
. Wouldn’t it be great if there was a
simple function to convert from what we have into what we want - i.e. [State
Int Int] -> State Int [Int]
.
It turns out that lists implement the typeclass Traversable
. If we look at
the definition of Traversable
then we see this:
class (Functor t, Foldable t) => Traversable (t :: * -> *) where
traverse :: Applicative f => (a -> f b) -> t a -> f (t b)
sequenceA :: Applicative f => t (f a) -> f (t a)
mapM :: Monad m => (a -> m b) -> t a -> m (t b)
sequence :: Monad m => t (m a) -> m (t a)
If we represent our list with Traversable t
and our State
monad with Monad
m
, then the function we after would have the type t (m a) -> m (t a)
-
that’s exactly the type of sequence
in the typeclass above! Let’s try using it:
addAnIncrementingAmount :: [Int] -> [Int]
addAnIncrementingAmount numbers =
evalState (sequence (map addAmount numbers)) 0
addAmount :: Int -> State Int Int
addAmount number amountToAdd number = do
amountToAdd <- get
modify (+1)
return (number + amountToAdd)
This works exactly as expected. In fact, this combination of map
and
sequence
is so useful, that if we look at the type signatures long enough, we
see that mapM
in Traversable
is the composition of these two functions:
mapM :: (a -> m b) -> t a -> m (t b)
Let’s try mapM
:
addAnIncrementingAmount :: [Int] -> [Int]
addAnIncrementingAmount numbers =
evalState (mapM addAmount numbers) 0
addAmount :: Int -> State Int Int
addAmount number amountToAdd number = do
amountToAdd <- get
modify (+1)
return (number + amountToAdd)
At this point, I hope that I’ve managed to practically show how State
and
mapM
can be used together without digging too deeply into the details
(there’s plenty of excellent resources on this elsewhere 2). However, my
example so far is not that useful, let’s look at something a bit more complex.
The Bowling Game Kata
The Bowling Game Kata is a well-know Test-Driven Development exercise where the goal is to calculate the score of a game of ten-pin bowling.
A typical Javascript solution might look something like this:
class Game {
constructor() {
this.rolls = [];
}
roll(pins) {
this.rolls.push(pins);
}
score() {
let score = 0;
let framePointer = 0;
for (const frame of Array(10).keys()) {
if (this.isStrike(framePointer)) {
score += this.scoreStrike(framePointer);
framePointer += 1;
} else if (this.isSpare(framePointer)) {
score += this.scoreSpare(framePointer);
framePointer += 2;
} else {
score += this.scoreNormalFrame(framePointer);
framePointer += 2;
}
}
return score;
}
isStrike(framePointer) {
return this.rolls[framePointer] === 10;
}
isSpare(framePointer) {
return this.rolls[framePointer] + this.rolls[framePointer + 1] === 10;
}
scoreStrike(framePointer) {
return 10 + this.rolls[framePointer + 1] + this.rolls[framePointer + 2];
}
scoreSpare(framePointer) {
return 10 + this.rolls[framePointer + 2];
}
scoreNormalFrame(framePointer) {
return this.rolls[framePointer] + this.rolls[framePointer + 1];
}
}
The score
function has a for
loop in it so this is the bit which will need
to be re-written to convert it to Haskell. Let’s start by extracting the
scoring of each frame and then add up the results at the end.
score() {
let frameScores = [];
let framePointer = 0;
for (const frame of Array(10).keys()) {
const [frameScore, rollsInFrame] = this.scoreFrame(framePointer);
frameScores.push(frameScore);
framePointer += rollsInFrame;
}
return frameScores.reduce((score, frameScore) => score + frameScore);
}
scoreFrame(framePointer) {
if (this.isStrike(framePointer)) {
return [this.scoreStrike(framePointer), 1];
} else if (this.isSpare(framePointer)) {
return [this.scoreSpare(framePointer), 2];
} else {
return [this.scoreNormalFrame(framePointer), 2];
}
}
Now we’ve extract scoreFrame
we can convert it to Haskell by replacing the
for
loop with a foldl
(the implementation below isn’t like for like, but
it’s close enough):
score :: [Int] -> Int
score rolls = sum $ fst $ foldl scoreFrame ([], rolls) [1..10]
scoreFrame :: ([Int], [Int]) -> Int -> ([Int], [Int])
scoreFrame state@(frameScores, rolls) _frame
| isStrike rolls = scoreStrike state
| isSpare rolls = scoreSpare state
| otherwise = scoreNormal state
isStrike :: [Int] -> Bool
isStrike rolls = allPins $ scoreRolls 1 rolls
isSpare :: [Int] -> Bool
isSpare rolls = allPins $ scoreRolls 2 rolls
scoreStrike :: ([Int], [Int]) -> ([Int], [Int])
scoreStrike (frameScores, rolls) =
(frameScores ++ [scoreRolls 3 rolls], moveFrame 1 rolls)
scoreSpare :: ([Int], [Int]) -> ([Int], [Int])
scoreSpare (frameScores, rolls) =
(frameScores ++ [scoreRolls 3 rolls], moveFrame 2 rolls)
scoreNormal :: ([Int], [Int]) -> ([Int], [Int])
scoreNormal (frameScores, rolls) =
(frameScores ++ [scoreRolls 2 rolls], moveFrame 2 rolls)
scoreRolls :: Int -> [Int] -> Int
scoreRolls numRolls rolls = sum $ take numRolls rolls
moveFrame :: Int -> [Int] -> [Int]
moveFrame numRolls rolls = drop numRolls rolls
allPins :: Int -> Bool
allPins = (== 10)
We can see here that there’s a lot of confusing looking type signatures.
Although this could be greatly improved by using explicit types rather than
Int
everywhere, there is still a lot of noise from having to manually thread
the state through everywhere.
Refactoring to Use State
Now lets take a look at a version which makes use of State
:
import Control.Monad (mapM)
import Control.Monad.State (State, get, put, modify, evalState)
import Control.Conditional (condM, otherwiseM)
score :: [Int] -> Int
score rolls = sum $ evalState (mapM scoreFrame [1..10]) rolls
scoreFrame :: Int -> State [Int] Int
scoreFrame _frame =
condM [ (isStrike, scoreStrike)
, (isSpare, scoreSpare)
, (otherwiseM, scoreNormal)
]
isStrike :: State [Int] Bool
isStrike = allPins <$> scoreRolls 1
isSpare :: State [Int] Bool
isSpare = allPins <$> scoreRolls 2
scoreStrike :: State [Int] Int
scoreStrike = do
score <- scoreRolls 3
moveFrame 1
return score
scoreSpare :: State [Int] Int
scoreSpare = do
score <- scoreRolls 3
moveFrame 2
return score
scoreNormal :: State [Int] Int
scoreNormal = do
score <- scoreRolls 2
moveFrame 2
return score
scoreRolls :: Int -> State [Int] Int
scoreRolls numRolls = sum . take numRolls <$> get
moveFrame :: Int -> State [Int] ()
moveFrame numRolls = modify (drop numRolls)
allPins :: Int -> Bool
allPins = (== 10)
While there are a few bits in this version which require some understanding, it does remove a lot of the clutter from the previous version. It also makes a clear separation between the state and the frame number itself (which incidentally is never actually used).
Final Worlds
I don’t claim that using mapM
instead of foldl
is always clearer, and I
don’t claim that either of the implementations of the Bowling Game scoring
exercise above is the best way to solve this in Haskell, but I do hope you have
enjoyed this article and perhaps learned something new along the way.
Featured photo by Michelle McEwen on Unsplash.
-
Haskell Programming from first principles and Learn You Some Haskell both explain these concepts from the ground up. ↩