Happy Learn Haskell Tutorial Vol 1

Buy now at Leanpub

Please Buy now at Leanpub

Written and illustrated by GetContented.

Published on 2017-07-08.


Contents

Main Table of Contents
Previous chapter: 18. Times-Table Train of Terror
19. Skwak the Squirrel
... 19.1. More on the ($) Function
... 19.2. Mid-Lesson Homework
... 19.3. Continuing On
... 19.4. Problems and Homework
Next chapter: 20. Basic Input

19. Skwak the Squirrel 🔗

Games! We saw the Fridge game. It took place in one single “room” which made it very limited. Then, the Train game didn’t really give you any freedom, but at least it had more rooms.

Next we’ll see a game that lets us imagine that we’re a squirrel and we live in a tree. Our new game will only have two areas, but it will provide more capability than before. And, in the process, we’ll get some more practice with all the things we’ve seen so far.

If we wanted to make a game that is a tiny bit more like a real text adventure game, it’d have to let the player move around between its game areas.

To keep it ultra-simple, let’s say we (our squirrel) could be able to go between only the inside and outside of the tree, perhaps. The program we’ll be discovering in this section is a lot more complicated than the Fridge game, but it’s about one of the most simple, basic text adventures possible.

Let’s look at the types first:


data GameObject = Player
                | Acorn
  deriving (Eq, Show)
data Room =
  Room Description [GameObject]
    deriving (Show)
type Description = String
type Inventory = [GameObject]
type GameMap = [Room]
type GameState = (GameMap, Inventory)

We have a GameObject type whose values can be either Player or Acorn. Fairly straightforward, this is just a sum type like we’ve seen before. What about deriving (Eq, Show), though? Well, this is a way to make a type become an instance of these typeclasses without having to write the code for it manually ourselves. Being an instance of Eq means we can use (==) and other comparison functions on values of this type, and being an instance of Show means it can be converted to strings with the show function.

Now, the Room type is a product type and it has a type constructor called Room as well as a value constructor of the same name, which is used to make values of the Room type. It has fields of Description, and a list of type GameObject which is used to hold the contents of the Room. So, because of the way the GameObject type is defined, a Room can have one or more Player or Acorn values in it. Our game will only ever have one of each in the whole game.

Next are a bunch of type synonyms which should be pretty easy to understand by now, possibly with the exception of GameState which is a 2-Tuple (otherwise known as a pair) of GameMap, and Inventory, which is a list of GameObject. GameMap is a list of Room.

The GameState type will be what we use to store all the changing pieces of our game as the player plays it. The GameMap will be all the rooms in the game, and the Inventory is what the player is holding moment by moment.

Let’s see a definiton for the initial state of the game and the main function, that will start the game.


initialState :: GameState
initialState =
  ( [ Room "You are inside a tree." [Player]
    , Room "You are outside of a tree." [Acorn]]
  , [] )

main :: IO ()
main = do
  putStrLn "Welcome to Skwak the Squirrel."
  putStrLn "You are a squirrel."
  gameLoop initialState

Now we see the main function, which as we know is the IO action that will be executed by Haskell when we compile and run our program.

All it does it announce the game, then runs a function called gameLoop using the intialState which is the state that the game should start with. The gameLoop contains the bulk of the program, and we’ll see it in a moment.

First, Let’s look at initialState. This is a GameState, which as we know from the types is a 2-Tuple that has a list of Room then a list of GameObject. We start our game with two rooms... we use the Room constructor to build the rooms. We have the outside and the inside of the tree. The Player is inside, and the Acorn is outside.

Let’s look at the gameLoop function now:


gameLoop :: GameState -> IO ()
gameLoop (rooms, currentInv) = do
  let currentRoom =
        case findRoomWithPlayer rooms of
        Just r -> r
        Nothing -> error $ "Somehow the player "
                         ++ "ended up outside the map!"
      possibleCmds =
        validCommands currentRoom currentInv
  if playerWon (rooms, currentInv)
  then gameOverRestart
  else do
    describeWorld currentRoom currentInv possibleCmds
    takeActionThenLoop
      currentRoom currentInv possibleCmds rooms

This function takes a GameState and returns an IO action. It’s the main functionality of the game. Each move the player makes goes through the game loop once, which is why it’s called a loop, because it’s like a circle.

We see that we can have a let expression in our do block which essentially sets up temporary variables in the do block from that point onwards.

We’re finding and grabbing the current room from the list of rooms, or throwing up our hands if it can’t find the Player.

Then we work out which commands are valid for the current room and the current inventory. Some player commands are only possible with some combination of Acorn being in the room with the player, or in the inventory.

Once that is done, we check if the player has won with an if expression, and if so, we tell the player they’ve won and offer to play again. If they didn’t win, then we describe the world to them at the room they’re in, and let them take an action by grabbing their command, then go back again to the start (using the takeActionThenLoop function).

Now we’ll go through the remaining functions. Note, though, that we can use any expressions (such as if) with no trouble in a do block for IO, even other nested do blocks, so long as those expressions result in an IO value of some kind. We’ll see in a moment that all of those functions we just discussed yield IO values of some kind.


findRoomWithPlayer :: [Room] -> Maybe Room
findRoomWithPlayer rooms =
  L.find (\(Room _ obs) ->
            any (== Player) obs)
         rooms

We can tell by the name that findRoomWithPlayer should return the Room whose objects include the Player. We’re returning a Maybe Room because it’s technically possible that the Player might not be in any Room. If that’s the case, there’s a problem, because the game won’t work. That’s why there’s an application of the error function in gameLoop if findRoomWithPlayer can’t find a Player.

We’re using a lambda as our Room-testing function here. The way find works is it looks through the list passed into it, applies the predicate section function (== Player) to each item, and if it finds one that returns True, it returns that one, wrapped in the Just data constructor from the Maybe type. If it doesn’t, it returns the Nothing value (also from the Maybe type.

In depth, our lambda takes a Room, pulls it apart using pattern-matching to get at the objects (which is a list of GameObject), names that obs, and then passes that to any, which will check to see if any of them return True for the function (== Player) which checks to see if something is equal to the Player value.

Next we’ll see the function that crafts the valid commands for a particular room and inventory combination:


validCommands :: Room -> Inventory -> [String]
validCommands (Room _ gameObjs) invItems =
    ["go"] ++ takeCommandList
    ++ dropCommandList ++ ["quit"]
  where
    takeCommandList =
      if somethingToTake gameObjs
      then ["take"]
      else []
    dropCommandList =
      if length invItems > 0
      then ["put"]
      else []

This function takes a Room and an Inventory and returns a list of String values that are all the commands that a player can type in depending on the current data: whether there is anything to take (in the room), or drop (from inventory).

It’s using the list concatenation operator (++) to join the String lists together into one list to return, and two definitions with if expressions to ensure we only put “take” or “put” on the command list if it’s appropriate.


somethingToTake :: [GameObject] -> Bool
somethingToTake objs =
  any (/= Player) objs

Of course, we can eta-reduce this function as we know by now (that is, get rid of the repeated trailing arguments):


somethingToTake :: [GameObject] -> Bool
somethingToTake = any (/= Player)

Here we take a list of GameObject which is used by validCommands as the list of objects in the room that can be taken. We’re using it to work out if there is anything that is not a Player in the list. The any function takes a predicate function, evaluates it on each item of a list, and returns True if any of the evaluations return True, otherwise False. The function (/=) means not equal to, and comes from the Eq typeclass. The end effect answers the question “Are there any non-player objects in this room?”.

As we’ve seen before, the use of parentheses around (/= Player) makes it into what’s called a section: it creates a function of one argument, having applied one of the 2-arguments required by the operator. Here we’re creating a function that takes a GameObject and returns a Bool that indicates whether the GameObject was Player.


playerWon :: GameState -> Bool
playerWon (rooms, currentInv) =
    any hasAcornAndInside rooms
  where hasAcornAndInside (Room desc objs) =
          desc == "You are inside a tree."
          && any (==Acorn) objs

This function answers the question “Has the player won yet?” The player has won if the acorn is in the tree (and not in the player’s inventory). Pay careful attention to the indentation (where the lines start). This matters!

We test for this by pulling the GameState tuple apart into a variable each for rooms and current inventory. We then go through all the rooms, and using the any function again, check if there’s one which has the description “You are inside a tree.” that also has the Acorn in its objects list.

Note the use of the && operator, which takes two Bool} expressions and returns \{True if they’re both True. This is called the logical and operator. It’s one way we can connect up logic expressions into larger chunks of meaning. There is also an (||) operator that is logical or, which will return True if either of its inputs evaluate to True, and a not function that takes only one Bool expression and returns the logical inverse of it (that is, if it’s True, it returns False and vice-versa).

Our playerWon function is a not the best way we could write it, because we’re relying on the description of the room to check if it’s the inside room. This gives us a lot of room for errors to creep in by accident. We did this to keep the types a bit simpler as you’re learning. It’s bad because what if we changed the description in the actual Room data, but then forgot to change this check String. It would mean it’d be impossible to win the game.

A better way would be to pull this description into its own definition that is defined in only one place. The best way, though, would be to have an identifier in the Room data type representing the type of Room it is, and to check against this to see if this Room was the winning goal Room.


gameOverRestart :: IO ()
gameOverRestart = do
  putStrLn $ "You won!"
    ++ "You have successfully stored the acorn"
    ++ " for winter. Well done!"
  putStrLn "Do you want to play again? y = yes"
  playAgain <- getLine
  if playAgain == "y"
  then gameLoop initialState
  else putStrLn "Thanks for playing!"

Our function to check if the game is now over uses a do block to print some messages congratulating the player, then asks if they want to play again, collects a line of input from them, and restarts the game from the beginning by using initialState if their input equals "y". Very straight-forward!


getCommand :: IO String
getCommand = do
  putStrLn "What do you want to do?"
  getLine

getCommand is a simple little action that prints a query to the player, gets that response and returns it. The type is IO String, which is the same type as getLine. Notice that as getLine is the last line of the function, we don’t need to use anything to pull the value out here, because it has the return type already. We’ll see how it’s used now in takeActionThenLoop:


takeActionThenLoop :: Room ->
                      Inventory ->
                      [String] ->
                      [Room] ->
                      IO ()
takeActionThenLoop currentRoom
                   currentInv
                   possibleCmds
                   rooms =
  do
    command <- getCommand
    if any (==command) possibleCmds
    then case command of
      "go" ->
          do
            putStrLn "You go..."
            gameLoop $ movePlayer (rooms, currentInv)
      "take" ->
          do
            putStrLn "You take the acorn..."
            gameLoop $
              moveAcornToInventory (rooms, currentInv)
      "put" ->
          do
            putStrLn "You put the acorn down..."
            gameLoop $
              moveAcornFromInventory (rooms, currentInv)
      "quit" ->
          putStrLn $ "You decide to give up."
                   ++ " Thanks for playing."
      _ ->
          do
            putStrLn "That is not a command."
            gameLoop (rooms, currentInv)
    else do
      putStrLn $ "Command not possible here,"
               ++ " or that is not a command."
      gameLoop (rooms, currentInv)

Even though this is one of the largest functions in the program, it’s reasonably simple. It’s structured by firstly taking a command from the user, then checking if the command is one of the valid ones for the player right now. If not it complains that it can’t understand and just goes back to the gameLoop with the current data. However, if it does actually match against a valid command, it runs through the case expression trying to find a match.

Each of the commands has a corresponding function which results in a modified GameState, which is then passed back into the gameLoop function for a further turn. We’ll see each of these functions momentarily.

Notice that we use the ($) function all over the place, such as to apply gameLoop to the result of the evaluation of the command functions, such as movePlayer.

Also notice that there is a pattern match for “_”, which for completeness will match on any other values for the command. In our case there aren’t actually any other commands, but not having this is a potential source of problems in the future as things change, and Haskell will complain if we leave it off. Take a moment and think what would happen if you added a new command to the validCommands function, but didn’t add it to the takeActionThenLoop function. If we keep this underscore catch-all, then play the game, the game will tell us that our new command is not recognised as a command. If we leave it off, though, our program will crash. That’s why it’s better to have total functions.

Having said this, a better solution would be to represent our commands with an algebraic data type. For now and to keep things slightly more simple, we’ll stick to the String type.

These next three functions are to adjust the game state when the player does something.


movePlayer :: GameState -> GameState
movePlayer (rooms, inv) =
    (newRooms, inv)
  where
    newRooms =
      map adjustRooms rooms
    adjustRooms (Room d objs) =
      if any (==Player) objs
      then (Room d (filter (/=Player) objs))
      else (Room d (Player : objs))

moveAcornToInventory :: GameState -> GameState
moveAcornToInventory (rooms, inv) =
    (newRooms, newInv)
  where
    newInv =
      Acorn : inv
    newRooms =
      map adjustRooms rooms
    adjustRooms (Room d objs) =
      Room d (filter (/=Acorn) objs)

moveAcornFromInventory :: GameState -> GameState
moveAcornFromInventory (rooms, inv) =
    (newRooms, newInv)
  where
    newInv =
      filter (/=Acorn) inv
    newRooms =
      map adjustRooms rooms
    adjustRooms (Room d objs) =
      if any (==Player) objs
      then Room d (Acorn : objs)
      else Room d objs

Our game only has two rooms, so the movePlayer function just goes over “all” two rooms using the map function, and applies a function that takes the Player out if it’s there using filter, or puts it in if it’s not there, using the list-prepend function (:). It does this by pattern-matching the Room into its pieces, then reconstructing it with a new GameObject list.

The moveAcornToInventory function returns a new 2-tuple (a GameState) that is comprised of an adjusted set of rooms by mapping a deconstructing-reconstructing function that uses filter to remove all acorns from the rooms’ GameObject list (there should only be one anyway), and an adjusted inventory by prepending an Acorn to it using (:). This “moves” the Acorn into inventory.

The moveAcornFromInventory does the reverse of this. The mapping function that puts the Acorn into the room where the player is is quite interesting because it has an if expression to detect if a particular room’s game objects contains the Player, and if so, it prepends an Acorn, otherwise it just re-builds the passed in room as it was before.


describeWorld :: Room ->
                 Inventory ->
                 [String] ->
                 IO ()
describeWorld currentRoom
              currentInv
              possibleCmds =
  do
    putStrLn $ describeRoom currentRoom
    putStrLn $ describeInventory currentInv
    putStrLn $ describeCommands possibleCmds

This is pretty straightforward, it just uses other functions to describe to the player which room they’re in, what they’re carrying and also which commands they can use here.

Notice, again, that we’re using another do block to make a single IO action out of a bunch of putStrLn function applications.

19.1. More on the ($) Function 🔗

Also we see the function ($) again at work, applying the function on the left of it to the result of evaluating everything on the right of it. The function application operator is often used as above, to avoid having to type brackets around expressions, as you probably know by now. It has the lowest possible precedence, which means it’s evaluated last, after all the other parts of an expression.

The type signature of this is ($) :: (a -> b) -> a -> b which means it takes a function from a to b (on the left of it) and an a (on the right of it), and applies the function to the “a” which gives a b. Obviously this is an extremely general function because ($) can apply to any function, and any value as long as they fit together.

Again, it’s important to realise that even though the type signature of ($) says it takes a function from a types to b types, it doesn’t have to be different types, but they can be if you like. So, (+3) fits the description even though its type is Num a => a -> a and this is because (a -> b) goes from one “any type” to another “any type”, so not necessarily the same type. (a -> b) is actually the type that fits any type of function at all!

There is no difference in result between writing (+3) $ 7 and writing (+3) 7, but when you start adding more arguments it starts to matter, like in (+3) $ 7 * 5 which gives 38, a different answer than (+3) 7 * 5, which is 50. From this we can see that ($) changes the order that things are evaluated in, just like putting parentheses around some things (so, (+3) $ 7 * 5 is equivalent to (+3) (7 * 5)).

19.2. Mid-Lesson Homework 🔗

Your homework (yes, mid-lesson homework) is to experiment with the ($) function, and explicit bracketing, and explore the differences and similarities if you can.

19.3. Continuing On 🔗

Next, the three functions that describeWorld uses:


describeRoom :: Room -> String
describeRoom (Room desc objs) =
  desc ++ if any (==Acorn) objs
          then " There is an acorn here"
          else ""

describeInventory :: Inventory -> String
describeInventory [] =
  "You are holding nothing"
describeInventory inv =
  "You are holding: " ++
  (concat $ map show inv)

describeCommands :: [String] -> String
describeCommands commands =
  "Commands: "
  ++ (L.intercalate ", " commands)

The describeRoom function takes a single Room and pattern-matches it into the desc and objs variables, which are the description of the Room, and the objects that the Room contains respectively. Notice that we’re using (++) to append the result of an if expression to the Room’s description here. The if expression uses a section. Remember that a section is effectively a partially applied operator. In this case, (==Acorn) :: GameObject -> Bool will check if something is an Acorn, and return a Boolean value for that (True/False).

Let’s think some more about the function any :: Foldable t => (a -> Bool) -> t a -> Bool for a moment. This is a function that takes a predicate from types a to Bool, a Foldable t a and then returns a Bool. This function will tell us if any item in the list “satisfies” the predicate. That is, if any item in the list returns True for our predicate function.

Or, partially applied to the (==Acorn) section as in any (==Acorn), it will tell us if there’s an Acorn in a supplied list! If there is an Acorn, it’ll say so. If not, just an empty String.

Next we’ll look at the describeInventory function. There are two definitions on this function. The first is for when there is nothing in the passed-in Inventory, so we’re matching on an empty list. This is simple.

The second definition is where the meat is. If the match with the empty list failed, that means there must be something in the Inventory, which we now pattern-match to the “inv” variable.

We then use the list concatenation operator (++) to append the result of an expression to the end of the beginning of a description of what they player is holding. Of course, this works because String is [Char], so therefore a list.

The expression we’re appending is (concat $ map show inv). We can break this apart into its pieces. Firstly, we now know about the ($) function, and can think about it like it’s bracketing like this: (concat (map show inv)).

Thinking about the expression map show inv, we can see that this takes the inv list and returns a new list that it builds by applying the show function to each GameObject in it, turning it into a list of strings. We can see this clearly if we check its type: map show :: Show a => [a] -> [String].

The function concat :: Foldable t -> t [a] -> [a] takes a Foldable-wrapped list of type a, and returns a list of type a. So in this case, because we’re passing it a [String], which as you know is the same as a [[Char]]. So that means the Foldable t that concat needs will be the list type, so a list of list of Char. The concat function will concatenate (join) that [String] to be a single String (or turn the [[Char]] into a [Char], same thing), by joining all the strings into one String.

The end result is that describeInventory will either tell the player they’re holding nothing, or that they’re holding a list of what they’re holding. The fact that there will only ever be one item in the list means we don’t need to comma-separate it or anything.

Next is the describeCommands function. It takes a list of commands, which are just String values, and calls the intercalate function from Data.List on that it. This is a pretty neat function. Its type is [a] -> [[a]] -> [a]. The first argument is a separator, and the second is a list of items of the same type. It then joins them all together into one big list after putting the separator between each of the items. So, here we’re using it to separate all the commands by comma-space so that it reads nicely.

19.4. Problems and Homework 🔗

While this game is designed to be small and simple for teaching purposes, it’s not hard to see that it would be pretty easy to adjust it to have a larger map, or more items. There are some problems, though.

Your homework is to go back through the chapter and think about what the problems of this program are as it’s built. For example, what would happen to the program if you added an additional room? Why not try it out and see.

It can be very useful to think about all the possible ways that programs could be changed, and how important it is to design programs in ways that make extending them easier, and also to make them resilient to change in as many ways as possible. This game is very easy to extend in some ways, but it’s not written to be extended at all in other ways.

Ideally when we write programs, we make it very easy to adjust them. We can do this by building them up from smaller pieces that closely match the type of data and functionality we want. For example, perhaps a List isn't a very good data type to use for our game map, and perhaps when we wrote our map movement functions, we could have written them in a way that meant they would work on any size List, not just a two-element one.

We can see from the language Haskell itself that its designers have followed this pattern because this language is very easy to extend, and encourages a style of programming where we build the language up to the problem we’re trying to solve.


If you’ve enjoyed reading this, please consider purchasing a copy at Leanpub today

Please follow us, and check out our videos Follow @HappyLearnTutes

Also, Volume 2 is now in beta and being written! Show your support and register your interest at its Leanpub site.


Main Table of Contents
Previous chapter: 18. Times-Table Train of Terror
19. Skwak the Squirrel
... 19.1. More on the ($) Function
... 19.2. Mid-Lesson Homework
... 19.3. Continuing On
... 19.4. Problems and Homework
Next chapter: 20. Basic Input