We use cookies and other tracking technologies to improve your browsing experience on our site, analyze site traffic, and understand where our audience is coming from. To find out more, please read our privacy policy.

By choosing 'I Accept', you consent to our use of cookies and other tracking technologies.

We use cookies and other tracking technologies to improve your browsing experience on our site, analyze site traffic, and understand where our audience is coming from. To find out more, please read our privacy policy.

By choosing 'I Accept', you consent to our use of cookies and other tracking technologies. Less

We use cookies and other tracking technologies... More

Login or register
to apply for this job!

Login or register
to save this job!

Login or register to start contributing with an article!

Login or register
to see more jobs from this company!

Login or register
to boost this post!

Show some love to the author of this blog by giving their post some rocket fuel 🚀.

Login or register to search for your ideal job!

Login or register to start working on this issue!

Engineers who find a new job through WorksHub average a 15% increase in salary 🚀

Blog hero image

A Rosetta Stone for Haskell Abstractions

Chas Leichner 10 May, 2018 | 23 min read

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.

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.

Java

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.

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 + "? ");
    }
}

Haskell

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.

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

Pointfree and <$>

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

State Record

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.

StateT

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

This 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 >>= &#95; -> 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 Ints. 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.

Originally published on reduction.io

Related Issues

viebel / klipse-clj
viebel / klipse-clj
  • Open
  • 0
  • 0
  • Intermediate
  • Clojure
viebel / klipse
  • Open
  • 0
  • 0
  • Intermediate
  • Clojure
viebel / klipse
  • 1
  • 0
  • Intermediate
  • Clojure
viebel / klipse
  • Started
  • 0
  • 1
  • Intermediate
  • Clojure
  • $80
viebel / klipse
  • Open
  • 0
  • 0
  • Advanced
  • Clojure
  • $80
viebel / klipse
  • Started
  • 0
  • 2
  • Advanced
  • Clojure
  • $180
viebel / klipse
  • Started
  • 0
  • 1
  • Intermediate
  • Clojure
viebel / klipse
  • 1
  • 1
  • Advanced
  • Clojure
  • $300

Get hired!

Sign up now and apply for roles at companies that interest you.

Engineers who find a new job through WorksHub average a 15% increase in salary.

Start with GithubStart with Stack OverflowStart with Email