It can be difficult at various stages of learning Haskell to see how the parts come together or how to use particular abstractions. This reference aims to ease that process by providing concrete examples of Haskell abstractions in a simple context. In particular, it demonstrates how abstractions are used by sequentially rewriting a program to do exactly the same thing using different techniques so that you can use your understanding of one code section to understand the new abstractions or techniques introduced in the next one. This is not intended as a Haskell tutorial in full, but it should answer questions once you have them. In addition, it is not intended as a primer for fancy type features and focuses more on term-level techniques in Haskell programming.
All of these programs implement a basic question and answer game that generates a sequence of addition or subtraction problems with random operands between 0 and 100. If the user answers correctly, it prints "Correct!" Otherwise, it prints out a message followed by the correct answer. In addition, it keeps track of how many questions the user got right or wrong and displays this after every round. If you want to have a go at implementing this, I would strongly suggest doing so now.
An example interaction follows:
Would you like to play? y/n: y
What is 40 + 95 ? 135
Correct!
You have solved 1 out of 1
Would you like to play? y/n: y
What is 8 + 71 ? 79
Correct!
You have solved 2 out of 2
Would you like to play? y/n: y
What is 36 + 49 ? 30
Sorry! the correct answer is: 85
You have solved 2 out of 3
Would you like to play? y/n: y
What is 73 - 12 ? 85
Sorry! the correct answer is: 61
You have solved 2 out of 4
Would you like to play? y/n: y
What is 54 + 87 ? 141
Correct!
You have solved 3 out of 5
Would you like to play? y/n: n
I think this is a good candidate for this sort of exercise because many new users find Haskell's treatments of non-termination, state, randomness, and user-interaction unintuitive and this program includes all of those while being simple enough that most people reading this shouldn't find the game logic confusing.
Note: I don't think many of these examples are actually idiomatic Haskell. They are far more complicated than they need to be for such a simple program. The intent is to use these examples to understand more complex programming techniques and then apply those techniques to far lager and more complex programs. In addition, this exercise isn't meant to show off Haskell in particular, since basically any language or style in common usage will do a good job with a small, simple program. Instead, simplicity and familiarity of the program is meant as a point of stability and understanding as more complex tools are introduced.
The first example is in Java to provide people with little or no Haskell experience a point of reference for what all of the other programs in this sequence do. Because this is intended as a starting point, anyone not already confident with basic Haskell programming should make sure they understand exactly what this program is doing before moving on.
import java.util.Random;
import java.util.Scanner;
public class RandomProblem {
public static void main(String[] args) {
int right = 0;
int rounds = 0;
Scanner keyboard = new Scanner(System.in);
Random rand = new Random();
while (keepPlaying(keyboard)) {
int x = rand.nextInt(100) + 1;
int y = rand.nextInt(100) + 1;
int solution = 0;
if (rand.nextBoolean()) {
solution = x + y;
printQuestion(x, '+', y);
} else {
solution = x - y;
printQuestion(x, '-', y);
}
rounds++;
if (solution == keyboard.nextInt()) {
System.out.println("Correct!");
right++;
} else {
System.out.println("Sorry! the correct answer is: " + solution);
}
System.out.println("You have solved " + right + " out of " +
rounds + " problems correctly.");
}
}
public static boolean keepPlaying(Scanner keyboard) {
System.out.print("Would you like to play? y/n: ");
return keyboard.next().toLowerCase().equals("y");
}
public static void printQuestion(int x, char op, int y) {
System.out.print("What is " + x + " " + op + " " + y + "? ");
}
}
The Haskell implementation of this program demonstrates a very common pattern in functional programming in which a stateful computation with a loop is replaced by a function that calls itself with updated parameters. In the same way that each time through a loop the state variables reflect the previous executions of the loop body, each time the function is called the parameters it is called with reflect the previous executions of the function. This potentially infinite recursion isn't a problem in Haskell because it is such a common pattern that the runtime was written with it in mind.
import Control.Monad
import Data.Char
import System.IO
import System.Random
gameLoop :: [Int] -> Int -> Int -> IO ()
gameLoop (x:y:r:values) right rounds = do
flushPut "Would you like to play? y/n: "
keepPlaying <- getLine
when (map toLower keepPlaying == "y") $ do
let (solution, opStr) = [(x + y, "+") , (x - y, "-")] !! (r `mod` 2)
flushPut $ unwords ["What is", show x, opStr, show y, "? "]
response <- readLn
let (total, message) = if solution == response
then (right + 1, "Correct!")
else (right, unwords ["Sorry! the correct answer is:", show solution])
putStrLn $ unwords
[message, "\nYou have solved", show total, "out of", show (rounds + 1)]
gameLoop values total (rounds + 1)
where
flushPut s = putStr s >> hFlush stdout
main :: IO ()
main = do
gen <- getStdGen
gameLoop (randomRs (1, 100) gen) 0 0
This program is structured in two parts. In main, the first function called, I set up the initial state of the program and call gameLoop
with the initial state. gameLoop
has three stateful things it is concerned with: a sequence of random numbers to turn into problems, the number of questions that have been answered correctly, and the total number of questions asked. Each one of these values is then passed as a parameter to the gameLoop
function, which is then updated when the function calls itself recursively after answering a question. There have been no questions answered and no questions asked at the start of the game, so right and rounds are both initialized to zero. The Java code gets random numbers from calling nextInt
and nextBoolean
repeatedly in order to get an infinite sequence of random numbers. In the Haskell version, I chose to create an infinite list of random values explicitly and pass it to the gameLoop
. It can then remove values from the list using pattern matching and use them as needed. Laziness ensures that the program doesn't try to evaluate an infinite number of random values.
The flushPut
function is defined here to ensure that output from the function is immediately seen by the user and isn't buffered.
If you are not comfortable with do-notation in Haskell: In the next few examples, anywhere that you see an expression like val <- ioVal
, the value on the right side (ioVal
) has type IO a
and the value on the left side (val
) has type a
. To make this concrete, in keepPlaying <- getLine
, getLine
has type IO String
and keepPlaying
has type String
. The way that do-notation and its related typeclass work ensures that you can't use it to write a function of type IO a -> a
, which would be a huge problem because you could use it to do IO anywhere, break a lot of programs, and confuse everyone. The value extraction with <-
works like that locally, but the type of the whole function most be of the form a -> ... -> IO b
for some b
. In these programs, b
is always ()
which is used in Haskell the way void
is used in Java. Stephen Diehl supplies more information here and here. In addition, I will supply a de-sugared version of some of the programs so you can compare and see that it all boils down to function application.
At many points in this document, I will specialized the types of various polymorphic functions in order to make them less abstract and thus easier to understand in context. By specialize, what I mean is replace type-level variables and typeclass instances with the specific types they are being used with which have the appropriate typeclass instances. For example, these are the polymorphic functions that do-notation desugars to:
>> :: Monad m => m a -> m b -> m b
>>= :: Monad m => m a -> (a -> m b) -> m b
And these are their specialized types:
>> :: IO a -> IO b -> IO b
>>= :: IO a -> (a -> IO b) -> IO b
And these are the (slightly specialized) types of the functions that interact with IO
and thus can be used with those functions:
getLine :: IO String
readLn :: IO Int
putStrLn :: String -> IO ()
putStr :: String -> IO ()
hFlush :: Handle -> IO ()
stdout :: Handle
This is the same program without the syntactic sugar for do-notation.
import Control.Monad
import Data.Char
import System.IO
import System.Random
gameLoop :: [Int] -> Int -> Int -> IO ()
gameLoop (x:y:r:values) right rounds =
flushPut "Would you like to play? y/n: " >>
getLine >>= \keepPlaying ->
when (map toLower keepPlaying == "y") $
let (solution, opStr) = [(x + y, "+") , (x - y, "-")] !! (r `mod` 2) in
flushPut (unwords ["What is", show x, opStr, show y, "? "]) >>
readLn >>= \response ->
let (total, message) = if solution == response
then (right + 1, "Correct!")
else (right, unwords ["Sorry! the correct answer is:", show solution]) in
putStrLn (unwords
[message, "\nYou have solved", show total, "out of", show (rounds + 1)]) >>
gameLoop values total (rounds + 1)
where
flushPut s = putStr s >> hFlush stdout
main :: IO ()
main =
getStdGen >>= \gen ->
gameLoop (randomRs (1, 100) gen) 0 0
<$>
This next version introduces use of the <$>
function. It can be thought of as a $
(function application) function that has been modified to work in more circumstances. The $
function performs low-precedence function application so that things like even (x + 3)
can be replaced with even $ x + 3
. It takes a function from a -> b
and an a
, which produces a b
by calling the function with the provided a
.
import Control.Applicative
import Control.Monad
import Data.Char
import System.IO
import System.Random
gameLoop :: [Int] -> Int -> Int -> IO ()
gameLoop (x:y:r:values) right rounds = do
flushPut "Would you like to play? y/n: "
keepPlaying <- ("y" ==) . map toLower <$> getLine
when keepPlaying $ do
let (solution, opStr) = [(x + y, "+") , (x - y, "-")] !! (r `mod` 2)
flushPut $ unwords ["What is", show x, opStr, show y, "? "]
correct <- (solution ==) <$> readLn
let (total, message) = if correct
then (right + 1, "Correct!")
else (right, unwords ["Sorry! the correct answer is:", show solution])
putStrLn $ unwords
[message, "\nYou have solved", show total, "out of", show (rounds + 1)]
gameLoop values total (rounds + 1)
where
flushPut = (>> hFlush stdout) . putStr
main :: IO ()
main = do
randomValues <- randomRs (1,100) <$> getStdGen
gameLoop randomValues 0 0
It is very common to want to operate on values "inside" of another type. So if we take a normal function, such as even
, we can directly call it on numbers (as in even 3
), but we can't call it on values that represent possible failure such as Maybe Int
because the Int
may be mising. We can achieve this by performing all of our logic "inside" the failure type Maybe a
. Correctly operating "inside" of a type representing failure means that if the type passed to the function represents failure, the return value of the function also represents failure. In this case, we have a type of Maybe Integer
and a function even :: Integer -> Bool
which doesn't know or care about the possibility of failure elsewhere in the system because Integer
can't fail, it can only be even or not even. To complete the example, calling even
on a possibly missing integer looks like even <$> Just 3
or even <$> Nothing
which evaluate to Just False
and Nothing
respectively.
These are the types of the function application functions for reference:
( $ ) :: (a -> b) -> a -> b
(<$>) :: Functor f => (a -> b) -> f a -> f b
In the case of Maybe a
, operation "inside" the type means that you can take a function from a
to b
(a -> b
) and turn it into a function from Maybe a
to Maybe b
(Maybe a -> Maybe b
). Maybe
can have two possible values, Just a
and Nothing
, so to implement this, the <$>
function needs to return Nothing
if the input is nothing, otherwise call the supplied function on the value held by Just
and wrap it in a Just
. <$>
also goes by the name fmap
and any type which has a Functor
instance implements what fmap (<$>)
means for that type. In short, if you use this machinery, Haskell will automate your null checks because the author of the Maybe
type explained how to null-check in general.
With that said, in this version of the code, I'm not doing anything particularly sophisticated using <$>
. I'm mostly using it to clean up some of the noise around directly assigning values of type IO a
to a variable just to extract the a
, like in keepPlaying <- getLine
. To me, this seems kind of pointless, like it doesn't reflect the actual logic. What I was thinking when writing that was "Check if the user input 'y'", not "Get a line from the user. See if the keepPlaying variable contains 'y'". It's not a big difference, but I find it annoying.
In this case, the type we are operating "inside" is IO
, and readLn
's value is IO Int
so we can reduce a bit of the noise that we pass to when
by using ("y" ==) . map toLower :: String -> Bool
with <$>
to call it with IO String
to produce an IO Bool
. After passing through the do-notation syntactic sugar, keepPlaying
holds a Bool value, which can be passed to when
. Similarly, we call (solution ==) with a value of IO Int
to produce an IO Bool
and then the Bool
is extracted with <-
. If the process of using <-
to get at a Bool
seems like a similar operation to working "inside" of types with <$>
, this isn't a coincidence: there is a deep relationship between the Functor
typeclass and the Monad
typeclass, if you would like to dig in, the Typeclassopedia is probably the best place to start. (Bartosz Milewski does a good job giving a taste of the theory as well.)
In my opinion, using <$>
frequently makes code simpler by removing extraneous intermediate values and reflecting the view of functors as lifted function calls, but it can obscure meaning especially when using the functor instances for common containers.
The other small style tweak I applied converted the flushPut
function to so-called pointfree form. Basically what this means is that I removed explicit mention of the variables passed to the function and expressed it entirely in terms of function composition. That is to say, I turned the function into a pipeline of components. As long as the pipeline isn't too complex, this can make functions clearer to read because you can be absolutely sure when reading them that they don't do anything fancy beyond plumbing their components together.
This is a little bit complicated by the fact that >>
is being partially applied to the value hFlush stdout
, so (>> hFlush stdout) :: IO () -> IO ()
is composed with putStr :: String -> IO ()
to produce a function of type (>> hFlush stdout) . putStr :: String -> IO ()
.
To specialize the type of the function composition function for this use, it looks like this: (.) :: (IO () -> IO ()) -> (String -> IO ()) -> (String -> IO ())
. What I mean is that if you look up the type of (.)
, it is (.) :: (b -> c) -> (a -> b) -> a -> c
which means that (.)
will work for any types that you choose for a
, b
, and c
. It can sometimes be hard to understand the type signatures when they are presented in such generality so it can be useful to plug in the specific types that are in use in the particular situation of interest. In this instance a
is String
, b
is IO ()
, and c
is IO ()
so the type of the whole function is (.) :: (IO () -> IO ()) -> (String -> IO ()) -> (String -> IO ())
.
- flushPut s = putStr s >> hFlush stdout
+ flushPut = (>> hFlush stdout) . putStr
Pointfree code can be simpler and easier to read, especially once you are familiar with the common idioms, but can obscure meaning when taken to an extreme.
In this version, I collected all of the state of a game into a single record, rather than passing in each parameter individually.
import Control.Applicative
import Control.Monad
import Data.Char
import System.IO
import System.Random
data Game = Game { values :: [Int], right :: Int, rounds :: Int }
updateGame :: Bool -> Game -> Game
updateGame correct Game { values = (_:_:_:remaining)
, right = score
, rounds = total } =
Game { values = remaining
, right = if correct then score + 1 else score
, rounds = total + 1 }
gameLoop :: Game -> IO ()
gameLoop gameState = do
flushPut "Would you like to play? y/n: "
keepPlaying <- ("y" ==) . map toLower <$> getLine
when keepPlaying $ do
let (x:y:r:_) = values gameState
let (solution, opStr) = [(x + y, "+") , (x - y, "-")] !! (r `mod` 2)
flushPut $ unwords ["What is", show x, opStr, show y, "? "]
correct <- (solution ==) <$> readLn
let gameState' = updateGame correct gameState
putStrLn $ if correct
then "Correct!"
else unwords ["Sorry! the correct answer is:", show solution]
putStr $ unwords
["You have solved", show $ right gameState', "out of",
show $ rounds gameState', "\n"]
gameLoop gameState'
where
flushPut = (>> hFlush stdout) . putStr
main :: IO ()
main = do
randomValues <- randomRs (1,100) <$> getStdGen
gameLoop Game { values = randomValues, right = 0, rounds = 0 }
This has the advantage of showing explicitly that the information passed to this function is all required for a single game, rather than coming from separate sources for separate aspects of the function. To follow this theme of concentrating state-specific code, I introduced an updateGame function which creates a new game record from an old one and the knowledge of whether the player won or lost.
At this point, I think the machinery is starting to overwhelm the essential complexity of the problem and probably wouldn't write code like this for something so simple under other circumstances. It's generally a good idea to use records if you find yourself passing the same arguments to several functions in your program or if you are using tuples with some sort of implicit meaning e.g. as 2D vectors. They do have some syntactic overhead so I wouldn't normally use one for just one function like this.
This section introduces Haskell's State
type and associated Monad
instance, as well as the StateT
monad transformer. Using StateT
removes the need to explicitly pass around the Game
state variables as explicit function arguments while still giving access to IO
operations.
import Control.Applicative
import Control.Monad
import Control.Monad.State
import Data.Char
import System.IO
import System.Random
data Game = Game { values :: [Int], right :: Int, rounds :: Int }
updateGame :: Bool -> Game -> Game
updateGame correct Game { values = (_:_:_:remaining)
, right = score
, rounds = total } =
Game { values = remaining
, right = if correct then score + 1 else score
, rounds = total + 1 }
gameLoop :: StateT Game IO ()
gameLoop = do
flushPut "Would you like to play? y/n: "
keepPlaying <- ("y" ==) . map toLower <$> liftIO getLine
when keepPlaying $ do
(x:y:r:_) <- gets values
let (solution, opStr) = [(x + y, "+") , (x - y, "-")] !! (r `mod` 2)
flushPut $ unwords ["What is", show x, opStr, show y, "? "]
correct <- (solution ==) <$> liftIO readLn
modify (updateGame correct)
gameState' <- get
liftIO . putStrLn $ if correct
then "Correct!"
else unwords ["Sorry! the correct answer is:", show solution]
liftIO . putStrLn $ unwords
["You have solved", show $ right gameState', "out of",
show $ rounds gameState']
gameLoop
where
flushPut = liftIO . (>> hFlush stdout) . putStr
main :: IO ()
main = do
randomValues <- randomRs (1,100) <$> getStdGen
evalStateT gameLoop (Game randomValues 0 0)
Most interesting programs that involve interaction require some amount of state to persist throughout their execution. As we previously saw, we can thread this state through an unbounded number of recursive calls to the same function in order to simulate a persistent state using only stateless functions. It can be tedious to explicitly pass additional extraneous variables to each function that needs the state, but we can take advantage of the fact that do-nation is strictly syntactic sugar over the >>=
operator from the Monad typeclass, and use it to thread our state record Game
around for us. The type connected to the implicit passing of a state type is suitably called State.
Previously, all do-notation was syntactic sugar for manipulation values of type IO
. We would like to keep doing I/O in this program, so I don't want to completely replace the IO
sugar with State
sugar. Instead, I use a type called StateT
to wrap the IO
type and produce a StateT Game IO ()
. After this conversion is done, the gameLoop :: Game -> IO ()
function no longer takes any explicit parameters and instead is a stateful value that holds a Game
state and can perform I/O, to write it in Haskell: gameLoop :: StateT Game IO ()
. In larger applications several monad transformers are frequently stacked together.
In general, the transformation from IO
to StateT Game IO
is extremely mechanical, I just added a liftIO
function to each function that returns IO
and made the types line up. The usual disclaimer about the low essential complexity of the problem applies here too. Using State
and StateT
to manage "mutable" state can be a good fit if you have a large number of functions that operate on the same state such a collection of parsing functions which update a shared symbol table. In addition, actual state mutation is available in the form of ST
if you want mutable state as a performance optimization. That said, implicitly passing state between functions can make them harder to understand, debug, and compose.
Operations like getLine
and putStrLn
have types IO String
and String -> IO ()
respectively and as such will not typecheck in an environment that expects values of type StateT Game IO a
for some a
. This is solved by using the liftIO :: IO a -> StateT Game IO a
function that transforms IO
-related things into StateT Game IO
-related things. Note: liftIO
is quite general and should work with any number of wrappings, I'm only giving a specialized type here in order to make the transformation between IO
types and State
-wrapped IO
type more explicit.
Finally, because the type of gameLoop
is StateT Game IO ()
, it can't be called directly in main :: IO ()
, and since the function is operating on an implicit state, it needs an initial state. The function evalStateT :: Monad m => StateT s m a -> s -> m a
takes an initial state and supplies it to the stateful computation that it represents, which then converts it into the underlying monad: in this case, IO
. Fully specialized, the function has the following type: evalStateT :: StateT Game IO () -> Game -> IO ()
.
To highlight particular parts of the conversion:
- correct <- (solution ==) <$> readLn
+ correct <- (solution ==) <$> liftIO readLn
- putStrLn $ if correct
+ liftIO . putStrLn $ if correct
- flushPut = (>> hFlush stdout) . putStr
+ flushPut = liftIO . (>> hFlush stdout) . putStr
- gameLoop Game { values = randomValues, right = 0, rounds = 0 }
+ evalStateT gameLoop (Game randomValues 0 0)
This is the same program without the do-notation syntactic sugar.
As a reminder, these are the specialized types of the desugared functions, used
in this example:
>> :: StateT Game IO a -> StateT Game IO b -> StateT Game IO b
>>= :: StateT Game IO a -> (a -> StateT Game IO b) -> StateT Game IO b
liftIO :: IO a -> StateT Game IO a
liftIO . putStrLn :: String -> StateT Game IO ()
import Control.Applicative
import Control.Monad
import Control.Monad.State
import Data.Char
import System.IO
import System.Random
data Game = Game { values :: [Int], right :: Int, rounds :: Int }
updateGame :: Bool -> Game -> Game
updateGame correct Game { values = (_:_:_:remaining)
, right = score
, rounds = total } =
Game { values = remaining
, rounds = total + 1
, right = if correct then score + 1 else score }
gameLoop :: StateT Game IO ()
gameLoop =
flushPut "Would you like to play? y/n: " >>
("y" ==) . map toLower <$> liftIO getLine >>= \keepPlaying ->
when keepPlaying $
gets values >>= \(x:y:r:_) ->
let (solution, opStr) = [(x + y, "+"), (x - y, "-")] !! (r `mod` 2) in
flushPut (unwords ["What is", show x, opStr, show y, "? "]) >>
(solution ==) <$> liftIO readLn >>= \correct ->
modify (updateGame correct) >> get >>= \game ->
(liftIO . putStrLn) (unwords [message solution correct,
"\nYou have solved", show $ right game, "out of", show $ rounds game]) >>
gameLoop
where
flushPut = liftIO . (>> hFlush stdout) . putStr
message _ True = "Correct!"
message solution _ = unwords ["Sorry! the correct answer is:", show solution]
main :: IO ()
main =
randomRs (1,100) <$> getStdGen >>= \randomValues ->
evalStateT gameLoop Game { values = randomValues, right = 0, rounds = 0 }
StateT
Clean-upThis is another minor clean-up version, but shows a couple of common (and not particularly arcane) practices.
import Control.Applicative
import Control.Monad
import Control.Monad.State
import Data.Char
import System.IO
import System.Random
data Game = Game { values :: [Int], right :: Int, rounds :: Int }
updateGame :: Bool -> Game -> Game
updateGame correct Game { values = (_:_:_:remaining)
, right = score
, rounds = total } =
Game { values = remaining
, rounds = total + 1
, right = if correct then score + 1 else score }
gameLoop :: StateT Game IO ()
gameLoop = do
flushPut "Would you like to play? y/n: "
keepPlaying <- ("y" ==) . map toLower <$> liftIO getLine
when keepPlaying $ do
(x:y:r:_) <- gets values
let (solution, opStr) = [(x + y, "+"), (x - y, "-")] !! (r `mod` 2)
flushPut $ unwords ["What is", show x, opStr, show y, "? "]
correct <- (solution ==) <$> liftIO readLn
game <- modify (updateGame correct) >> get
liftIO . putStrLn $ unwords [message solution correct,
"\nYou have solved", show $ right game, "out of", show $ rounds game]
gameLoop
where
flushPut = liftIO . (>> hFlush stdout) . putStr
message _ True = "Correct!"
message solution _ = unwords ["Sorry! the correct answer is:", show solution]
main :: IO ()
main = do
randomValues <- randomRs (1,100) <$> getStdGen
evalStateT gameLoop (Game randomValues 0 0)
I extracted a function from the if-expression because I think it's a bit more straightforward and compact and does a better job of breaking up the logical portion of the program.
- liftIO . putStrLn $ if correct
- then "Correct!"
- else unwords ["Sorry! the correct answer is:", show solution]
+ liftIO . putStrLn $ unwords [message solution correct,
+ message _ True = "Correct!"
+ message solution _ = unwords ["Sorry! the correct answer is:", show solution]
I also compacted the the two lines where I update the game state into one as follows:
- modify (updateGame correct)
- gameState' <- get
+ game <- modify (updateGame correct) >> get
Normally, the >>
operator is hidden behind the syntactic sugar provided by do-notation and indeed it is inserted between these two operations in the previous version. You can tell because modify
doesn't name its result using <-
and get
doesn't take any parameters beyond its shared state.
I chose to make this explicit rather than using the do-notation sugar to reflect the fact that I really want a state update operation that returns the new state because I want to keep computing with the new state locally even after modifying it.
>>
is an operator that sequences operations while retaining the behavior encoded by do-notation. To understand what this means, remember that do { x <- foo; bar x }
desugars to foo >>= \x -> bar x
, but since there are no variable bindings, do { baz; qux }
desugars to baz >>= \_ -> qux
, which is the same as baz >> qux
. Since >>=
is a method of the Monad
type class, it has a different meaning for each type class instance. In this case >>=
is auto-connecting an implicit state parameter so >> just runs two operations where the second operation doesn't depend on the result of the first, but the second operation does see any modifications the first operation made to their shared state. In this context the type of >>
is (>>) :: StateT Game IO () -> StateT Game IO Game -> StateT Game IO Game
which is () -> Game -> Game
lifted to respect the structure of StateT Game IO
.
lens
This section introduces the use of a lens library, Control.Lens -- henceforth known as lens
, which provides utility functions for manipulating data in a composable and generic way. If you have used the STL in C++, lens fits in a similar niche. Unlike the STL, it isn't standardized or official in any way with Haskell, it's just a library many people (including myself) like.
{-# LANGUAGE TemplateHaskell #-}
import Control.Applicative
import Control.Lens hiding (op)
import Control.Monad
import Control.Monad.State
import Data.Char
import System.IO
import System.Random
data Game = Game { _values :: [Int], _right :: Int, _rounds :: Int }
makeLenses ''Game
updateGame :: Bool -> Game -> Game
updateGame correct =
(values %~ drop 3) .
(rounds +~ 1) .
(right +~ if correct then 1 else 0)
gameLoop :: StateT Game IO ()
gameLoop = do
flushPut "Would you like to play? y/n: "
keepPlaying <- ("y" ==) . map toLower <$> liftIO getLine
when keepPlaying $ do
(x:y:r:_) <- use values
let (solution, opStr) = [(x + y, "+") , (x - y, "-")] !! (r `mod` 2)
flushPut $ unwords ["What is", show x, opStr, show y, "? "]
correct <- (solution ==) <$> liftIO readLn
game <- modify (updateGame correct) >> get
liftIO . putStrLn $ unwords [message solution correct,
"\nYou have solved", show $ game ^. right, "out of", show $ game ^. rounds]
gameLoop
where
flushPut = liftIO . (>> hFlush stdout) . putStr
message _ True = "Correct!"
message solution _ = unwords ["Sorry! the correct answer is:", show solution]
main :: IO ()
main = do
randomValues <- randomRs (1,100) <$> getStdGen
evalStateT gameLoop (Game randomValues 0 0)
All of the functions provided by lens
work using a type of generalized getter/setter functions called lenses. (Note: If this idea is interesting to you, but you don't want all of the bells and whistles in lens
, there are other, simpler lens libraries that use the same representation as lens
, such as lens-family
.) Lenses are extremely mechanical to produce for standard data types, so it includes some Template Haskell functions which will write them for you. Normally Haskell provides projection functions when you write a record which are named after the record fields (e.g. rounds :: Game -> Int
), so we start the names of record members with underscores so generated lenses won't conflict.
After converting to using lenses for field access, anything that touched the game state needs to be rewritten to use the new combinators. I think this does simplify the updateGame function, but the logic around printing becomes slightly worse.
The updateGame function previously used pattern matching to take apart the Game value and put it back together again. The version using lenses instead constructs three modification functions of type Game -> Game
and composes them together using .
(This is the same pipeline technique that was discussed in the context of flushPut). What goes into updateGame? We need to remove the first two random values because they were used for the last problem, we need to unconditionally increment the number of rounds, and if the user got the last problem correct, we should increment the number right. Let's look at each part separately because we know they don't interact due to their construction as composed functions.
The crux of updating values
is the %~
operator. This takes a lens and a function and creates a function that takes a record and modifies one of its fields using the provided function. The operator's function can be remembered as a pun on the common use of %
as the mod (or modular arithmetic) operator. To look at this concretely, in this instance %~
has type:
(%~) :: Lens' Game [Int] -> ([Int] -> [Int]) -> (Game -> Game)
We can use the values
lens that makeLenses built to satisfy the (Lens' Game [Int]) parameter leaving:
(values %~) :: ([Int] -> [Int]) -> (Game -> Game)
We want to remove the first two elements of the infinite list of random values so the ([Int] -> [Int]) parameter should be drop 2 :: [a] -> [a]
which will specialize to drop 2 :: [Int] -> [Int]
when used with lists of Int
s. Supplying the modification function produces the following:
(values %~ drop 2) :: Game -> Game
This is precisely a Game state update function which removes the first two entries in the values
list. If you aren't into the operator business, %~
also goes by the name over
so the previous function could be written over values (drop 2)
.
By this logic, the function for updating the rounds
could look like this, where the modification function is an increment function constructed through partial application of addition.
(rounds %~ (+1)) :: Game -> Game
This is a bit cluttered and likely very common, so lens
provides the +~
combinator that lets you modify a field by adding a number. (Its name should evoke the += operator from many imperative programming languages.) Using this the rounds update can be written as
(rounds +~ 1) :: Game -> Game
There isn't a named analog for +~
so if you don't like the lens
operators, this would be over rounds (+1)
.
Finally the number right can be updated using a variation on the same logic using the correct :: Bool
parameter passed into the updateGame function.
(right +~ if correct then 1 else 0)
Composing these together gives the complete updateGame function using lenses:
+ updateGame :: Bool -> Game -> Game
+ updateGame correct =
+ (values %~ drop 2) .
+ (rounds +~ 1) .
+ (right +~ if correct then 1 else 0)
- updateGame :: Bool -> Game -> Game
- updateGame correct Game { values = (\_:\_:\_:remaining)
- , right = score
- , rounds = total } =
- Game { values = remaining
- , rounds = total + 1
- , right = if correct then score + 1 else score }
Next, gets :: (Game -> a) -> StateT Game IO a
, which used the values :: Game -> [Int]
projection function, is replaced with use :: Lens' Game a -> StateT Game IO a
which uses the values :: Lens' Game [Int]
lens to get the values out of the state. These have precisely the same result, namely a value of type StateT Game IO [Int]
.
- (x:y:r:\_) <- gets values
+ (x:y:r:\_) <- use values
Finally, since lenses aren't projection functions, we have to update the message printing code as well. This is slightly more straightforward than in the case of use and get, because lens
provides an operator for turning lenses into projection functions, (^.) :: s -> Lens' s a -> a
or, specialized for this instance, (^.) :: Game -> Lens' Game Int -> Int
. If you squint a bit, you can see that applying a lens for a Game
field to the second argument will give you a function Game -> Int
which is just a normal projection function. In this case show $ right game
can be replaced with show $ game ^. right
or show $ view game right
, if you don't like the operators. In total, this leads to the following change:
- liftIO . putStrLn $ unwords [message solution correct,
- "\nYou have solved", show $ right game, "out of", show $ rounds game]
+ liftIO . putStrLn $ unwords [message solution correct,
+ "\nYou have solved", show $ game ^. right, "out of", show $ game ^. rounds]
In summary for this section, introducing lens
made the updateGame function slightly less verbose, but we didn't get any big wins. We'll see what this did for us once we make more use of the utility functions lens provides and have slightly more complex states to wrangle.
lens
with StateT
The combination of StateT and lens gives us the ability to easily directly modify the Game state using similar looking operations to imperative languages, in this version, we remove the use of modify
and updateGame
.
{-# LANGUAGE TemplateHaskell #-}
import Control.Applicative
import Control.Lens hiding (op)
import Control.Monad
import Control.Monad.State
import Data.Char
import System.IO
import System.Random
data Game = Game { _values :: [Int], _right :: Int, _rounds :: Int }
makeLenses ''Game
gameLoop :: StateT Game IO ()
gameLoop = do
flushPut "Would you like to play? y/n: "
keepPlaying <- ("y" ==) . map toLower <$> liftIO getLine
when keepPlaying $ do
(x:y:r:_) <- values <<%= drop 3
numRounds <- rounds <+= 1
let (solution, opStr) = [(x + y, "+") , (x - y, "-")] !! (r `mod` 2)
flushPut $ unwords ["What is", show x, opStr, show y, "? "]
correct <- (solution ==) <$> liftIO readLn
numRight <- right <+= if correct then 1 else 0
liftIO . putStrLn $ unwords [message solution correct,
"\nYou have solved", show numRight, "out of", show numRounds]
gameLoop
where
flushPut = liftIO . (>> hFlush stdout) . putStr
message _ True = "Correct!"
message soln _ = unwords ["Sorry! the correct answer is:", show soln]
main :: IO ()
main = do
randomValues <- randomRs (1,100) <$> getStdGen
evalStateT gameLoop (Game randomValues 0 0)
This approach explicitly states what can be manipulated and centralizes it into one type like in traditional (or more explicit) programming with stateless functions, but it uses Haskell library features to implicitly update the state as it's passed around.
Understanding the new code requires knowing two more pieces of lens operator grammar: just as any stateless update operator ends in ~
, stateful update operators end in =
and update an ambient state. In our case this state type is StateT Game IO ()
. What this means is that if you would write i += 1
to update the value of a variable in an imperative language, you can use i += 1
in Haskell to update the field named i
in your state record (in our case this record is Game). Additionally, you can can have a field-update operator return the new value of field by prepending <
. This means the Haskell/lens equivalent of x = ++i
is x <- i <+= 1
. Finally, the field-update operator will return the value before update if you prepend <<
, which makes x <- i <<+= 1
the Haskell/lens equivalent of x = i++
.
With these in hand, we can drop the first two values
after we access them using <<%=
instead of %~
as we did in updateGame. This will remove the first two elements of values
and return the list as it was before modification so we can extract the dropped values with pattern matching.
(x:y:r:_) <- values <<%= drop 3
The rounds and right
fields again use the same technique, but we replace +~
with <+=
so that we update the ambient Game
state, and get the new values for use in local computation.
numRounds <- rounds <+= 1
numRight <- right <+= if correct then 1 else 0
Since we can directly name the results of these updates, we can then remove the need to access the fields out of the updated game state and just use the results directly instead.
- liftIO . putStrLn $ unwords [message solution correct,
- "\nYou have solved", show $ game ^. right, "out of", show $ game ^. rounds]
+ liftIO . putStrLn $ unwords [message solution correct,
+ "\nYou have solved", show numRight, "out of", show numRounds]
This means that if at any point you want to understand the operation of the function for testing or debugging, you can still provide the appropriate state record and know that you have specified all of the information it depends on, but you don't have to worry about tracking these dependencies during other parts of development. That said you now have a stateful bummer to deal with, so use this stuff carefully.