In this week’s lab you will learn how to read somebody else’s code, study important syntactic expressions called Guards, and start implementing the Warehouse game in CodeWorld that constitutes your first assignment. Also, if you have not read through the first set of lecture notes from the live coding lectures please do so before you start this lab!

You will observe that this lab is longer and has more exercises than last week’s lab. We do not expect you to complete all the exercises during the lab. However, we expect you to work on the remaining exercises at home, complete all of them before the next lab and send the link with your solutions to your tutor! The lab of next week depends on your solutions. If you don’t complete them on time, you won’t be able to start the next lab!

1. Guards

The functions you’ve been working with so far are just simple mathematical calculations involving their inputs. But you can also write conditional functions in Haskell. Such functions contain syntax that allows to select among answers depending on the truth of certain conditions.

There are several options for these syntactic structures in Haskell — two important ones are case expressions and guarded expressions, also known as guards. We’ll start with guards and look at case expressions next week.

For this exercise we have created a file called Grades.hs for you. Click Grades to see the script on CodingGround. You will need to double-click on the file Grades.hs to see the script.

(Alternatively, you can click on Grades.hs to download the file to your local machine.)

The script Grades.hs contains code for calculating grades:

data Grades
  = Fail
  | Pass
  | Credit
  | Distinction
  | HighDistinction
  deriving (Show)

grade :: Integer -> Grades
grade mark
  | mark >= 80 && mark <= 100 = HighDistinction
  | mark >= 70 && mark < 80 = Distinction
  | mark >= 60 && mark < 70 = Credit
  | mark >= 50 && mark < 60 = Pass
  | mark >= 0 && mark < 50 = Fail
  | mark < 0 || mark > 100 = error "Program error: Not a valid mark"

Here, data is the Haskell keyword for defining a new enumeration type. It is followed by the name of the new type, followed by an equals sign, followed by a list of constructors of this type that are separated by bars. Type and constructor names always start with a capital letter.

The indented line deriving (Show) allows the values of the type to be displayed symbolically (e.g., Fail, Pass, Credit, etc.) by GHCi. This is entirely for readability and has nothing otherwise to do with the type.

Guarded expressions follow in the next lines. Each line | <guard> = <expression> of the function grade can be read as: if <guard> is true then the result is <expression>. Thus, a guarded expression is an expression that will only be reached if the associated guard (the Boolean expression right in front of it) evaluates to True.

Load the script (in the terminal, locally or on CodingGround) using GHCi:

ghci -Wall Grades.hs

Try calling the function with a range of arguments:

> grade 99
> grade 50

You will notice that when you load Grades.hs GHCi nags with a warning Pattern match(es) are non-exhaustive. To fix this error message, we can use the guard otherwise and associate an error message "Program error: Not a valid mark" with it. This will catch anything not explicitly enumerated by the preceding guards.

It is very important to place this guard in the last line of your code:

data Grades
  = Fail
  | Pass
  | Credit
  | Distinction
  | HighDistinction
  deriving (Show)

grade :: Integer -> Grades
grade mark
  | mark >= 80 && mark <= 100 = HighDistinction
  | mark >= 70 && mark < 80 = Distinction
  | mark >= 60 && mark < 70 = Credit
  | mark >= 50 && mark < 60 = Pass
  | mark >= 0 && mark < 50 = Fail
  | otherwise = error "Program error: Not a valid mark"

This is because guarded expressions in Haskell are evaluated in the order in which they appear. If your function starts with an otherwise guard, its evaluation will never go beyond this guard, because otherwise is just another word for True in Haskell!

Make the change to Grades.hs, save it, and reload the script in GHCi.

What happens if you put the otherwise line in different positions in your definition of grade. For example, place the otherwise line at the very beginning of your guarded expressions, reload your program in GHCi and test it. You will find that the function grade now always gives the same (sometimes undesired) answer, no matter what its arguments are! So don’t forget to make this line the last line of the grade function!

Since the guarded expressions are evaluated in sequence, we can have a more concise variant of the function for calculating grades that is as follows:

riskyGrade :: Integer -> Grades
riskyGrade mark
  | mark > 100 = error "Program error: mark above 100"
  | mark >= 80 = HighDistinction
  | mark >= 70 = Distinction
  | mark >= 60 = Credit
  | mark >= 50 = Pass
  | mark >=  0 = Fail
  | otherwise  = error "Program error: mark below 0"

This implementation, however, has an implicit drawback. Experiment with swapping the guarding expressions in riskyGrade and calling it with various arguments. For example, make the guarded expression mark >= 0 = Fail the first expression of your function and call it with 90. The function will output Fail despite the fact the mark is 90. Can you find an explanation for this?

The reason is that more than one guard in riskyGrade may be evaluated to True for a given input. For example, all guards except mark > 100 will be evaluated to True when the input is 90. Then the function outputs the result associated with the guarded expression that appears first in the function due to sequential evaluation. The order of guards matters!

In contrast for any input in grade there is one and only one guard that evaluates to True. In this sense guards are not overlapping. Furthermore, each guard (besides the otherwise guard) precisely and fully defines the desired outcome. For example, if we need to know how the Distinction grade is defined, it is sufficient to look at its guard. Due to this exhaustive definition of each of the guarded expressions their order does not matter in grade. We only have to take care that the otherwise guard appears at the very end.

It is always better to program in the safest way possible. Thus implementation grade is preferable to riskyGrade.

Below we slightly improve grade by distinguishing the case when an invalid mark is given and using the otherwise guard as an additional safety feature. The otherwise guard should never be reached — unless there is a fault in our logic.

grade :: Integer -> Grades
grade mark
  | mark >= 80 && mark <= 100 = HighDistinction
  | mark >= 70 && mark <   80 = Distinction
  | mark >= 60 && mark <   70 = Credit
  | mark >= 50 && mark <   60 = Pass
  | mark >=  0 && mark <   50 = Fail
  | mark <   0 || mark >  100 = error "Program error: Not a valid mark"
  | otherwise = error "Program error: Non-exhaustive guards in function: grade"

2. Coding Style

Always strive to create code that not only works, but is stylish and concise. Try to write small functions that perform just a single task, and then combine those smaller pieces to create more complex functions. Don’t repeat yourself: write one function for each logical task, and reuse functions as necessary.

Be sure to write functions with exactly the specified name and type signature for each exercise (to help us test your code). You may create additional helper functions with whatever names and type signatures you wish.

3. How to Analyse Code

Click NumWords to view the Haskell script from last week on CodingGround. Alternatively, download the same Haskell script NumWords.hs and open it in a text editor.

We don’t expect you to understand every detail in this script now. But you can already try to trace the evaluation steps defined by its functions. There are two basic strategies that you can follow:

  • Bottom-Up analysis: Start at the beginning of the program and try to read it as a list of more and more complex text constructors. The earlier functions (start where it says digits) are handling the more basic translations (like individual digits and ...teen numbers), while the later functions are handling the more complex assemblies. This strategy has the advantage that you can start with simple and completely defined functions first, yet you need to trust the programmer for a while that all this will eventually lead to the desired goal.
  • Top-Down analysis: Start with the function that you actually called — convertToWords — and follow the chain of operations it initiates. This means that if the programmer has defined things before they are used (enforced by many languages), that this function will naturally be found at the end of the program. Find it there and see which other functions it use to achieve its goal. You will find that this function usesconvert_orders which in turn uses link and convert_hundreds, which in turn uses convert_tens and split_order, etc. This will naturally lead you backwards in the program until you arrive at the most basic translations at the start of the program. This strategy has the advantage that you keep the purpose of the program in focus at all times.

4. CodeWorld

In this and the next few labs, you will be creating lots of pictures and animations in CodeWorld. Each lab will start with a Haskell script that you will augment with your own code.

The CodeWorld Guide contains descriptions of CodeWorld’s functions. Refer to this guide when necessary. You can access it by clicking Guide at the bottom left of the CodeWorld web interface.

This lab’s CodeWorld starting script is available at https://code.world/haskell#PdYwH3UdUZUi2zLwh7XGx0A

Submitting

You will submit all exercises in one file. Every exercise results in one top-level function called exercise1, exercise2, etc., of type IO (). To run a specific exercise, define main = exercise1 or main = exercise2.

Only submit code that compiles. If you cannot get it to compile, ask for help!

Remember to sign in to CodeWorld in order to be able to save your work. Once you are done, you should share the URL for your code in an e-mail to your tutor. To get a URL for your code, press the Share button.

Exercise 1: Traffic lights

Submission required: exercise1

In the live coding lecture, we defined a traffic light animation in CodeWorld. That traffic light had only red and green lights. However, most traffic lights also have a yellow light in the middle.

In this exercise you are asked to change the code to include the yellow light and animate the following sequence of traffic light signalling in a loop:

  • a long green phase
  • a short yellow phase
  • a long red phase
  • back to green

Here is how it should look:

The resulting program should be called exercise1.

Exercise 2: Blooming tree (required for COMP1130)

Submission required for COMP1130 (optional for COMP1100): exercise2

Only COMP1130 students are required to complete this exercise! COMP1100 students can skip it and move on to Exercise 3 (and perhaps come back to it later).

We defined a tree in the lecture, but it looks a bit lifeless. The code was:

tree :: Integer -> Picture
tree 0 = blank
tree n = path [(0,0),(0,1)] & translated 0 1 (
  rotated (pi/10) (tree (n-1)) & rotated (- pi/10) (tree (n-1)))

Make the tree bloom! Create an animation that looks like the lifeless tree 8 initially, but which then grows blossoms at the end of each twig within 10 seconds. After 10 seconds, the tree should be in full bloom and the animation should stop.

A bloom could be a yellow circle growing in size, or something more intricate with petals and better colours and whatnot.

In your code, modify tree to be abstract in the actual shape of the blossoms. This way, the tree function itself is independent of time. Do not pass the time parameter to the tree function!

The resulting program should be called exercise2.

Exercise 3 (Assignment 1, Part A): Warehouse maze

Submission required: exercise3

Over the next few labs, you will implement a transport puzzle called Warehouse. This will constituteyour first assignment. You must complete this Part A (and the other exercises) and submit to your tutor before next lab so they can see and comment on your progress. You may not get all the way through Exercise 3 during your lab. So, make sure save your work on CodeWorld so you can come back to it later.

Game rules

The game is played on a board of tiles. Each tile is either a floor or a wall. Floor tiles can be empty, can contain boxes, or can be marked as storage locations.

The player is confined to the board, and may move horizontally or vertically onto empty squares (never through walls or boxes). The player can also mover into a box, which pushes it into the square beyond. Boxes may not be pushed into other boxes or walls, and they cannot be pulled. The goal is to relocate all the boxes to storage locations. The puzzle is solved when this is accomplished.

Here is an example of a Warehouse board and of a puzzle being solved:

Warehouse puzzle being solved

Drawing the maze

In part one of your assignment we will draw the board/maze. From the rules, we can conclude that the following tiles are necessary:

  1. Walls
  2. Ground (empty floor tiles)
  3. Ground marked as storage
  4. Boxes

We have already provided possible implementations of wall, ground, storage and box for you. Analyse them carefully. Each of them is of type Picture and draws a corresponding tile. Each tile has width and height of 1 and is positioned at the centre of the picture.

Note that we have defined the following datatype in the script:

data Tile = Wall | Ground | Storage | Box | Blank

The new enumeration type Tile now consists of exactly these five values, no more and no less. So the type is tight. Importantly, every value is self-explanatory.

Now create a function

drawTile :: Tile -> Picture

which, given an argument of type Tile draws the corresponding tile. For example drawTile Storageshould draw a storage tile. If the argument is neither of Wall, Ground, Storage, or Box, the function should not crash; it can draw a blank tile and CodeWorld has blank reserved for such occasions.

A maze can be represented as a function with the type Integer -> Integer -> Tile: given two coordinates it returns the type of the tile present there. Coordinates (0,0) correspond to the middle of the picture.

We have provided a possible simple maze for you:

maze :: Integer -> Integer -> Tile
maze x y
  | abs x > 4  || abs y > 4    = Blank
  | abs x == 4 || abs y == 4   = Wall
  | x == 2 && y < 0            = Wall
  | x == 3 && y < 0            = Storage
  | x > -3 && x < 1 && y == 1  = Box
  | otherwise                  = Ground

with its corresponding image:

Analyse the code of the maze very thoroughly and understand how it encodes the image. Note that the maze itself appears within the following intervals of coordinates xx and yy: 4x4−4≤x≤4 and 4y4−4≤y≤4. What do you think is the purpose of the first line abs x > 4 || abs y > 4 = Blank then? If you cannot answer this question right now, keep it in mind; comment this line out of your program when you have completed this lab and see what happens.

Now think how maze and drawTile can be used together (Hint: one can be applied to the other). When you figure this out, define the function

drawTileAt :: Integer -> Integer -> Picture

which, given two coordinates, draws a tile located at these coordinates. You might need to use fromIntegral in order to convert coordinates from type Integer to a floating-point type.

Now you have a function that draws individual tiles of your maze. It remains to actually draw the whole maze itself. Define a picture

pictureOfMaze :: Picture

that draws the whole maze.

A naive way of defining pictureOfMaze is to include at least 9×9=819×9=81 invocations of drawTile, one for each possible coordinate. You can imagine how cumbersome such a function would be! As you learned in the lecture, recursive functions are more concise and elegant. Come up with a recursive definition of pictureOfMaze. Feel free to define any additional functions that might be of help.

Hint: Remember from the live coding lecture of week 1 that the coordinate plane of CodeWorld is limited by coordinates xx and yy: 10x10−10≤x≤10 and 10y10−10≤y≤10. This might be useful for calling your recursive functions.

The resulting program should be called exercise3.

Finishing up

Remember to sign in to CodeWorld in order to be able to save your work. Once you are done, share the URL for your code in an e-mail to your tutor. To get a URL for your code, press the “Share” button.