teaching machines

CS 330 Lecture 29 – Haskell IO Cont’d

April 17, 2017 by . Filed under cs330, lectures, spring 2017.

Dear students,

Last time we introduced ourselves to Haskell’s world of purity and its world of side effects. We learned that any value that gets created in the world of side effects is packaged up in an IO wrapper using the return function. We can unpackage such wrappers using the <- operator. We will finish out our discussion of Haskell IO today.

Let’s deal with command-line parameters. This, like getting the username, involves communicating with the process, which is a stateful thing. We enter the world of side effects. The getArgs function in System.Environment has this signature:

getArgs :: IO [String]

Let’s extract out the first argument and turn into an Int:

main = do
  args <- getArgs              -- unwrap from the IO
  let arg0 = head arg          -- pull out just the first
  let n = read arg0 :: Int     -- parse the int

Erm… We should do something interesting here. Let’s print out all the factors of n. How can we do this in Haskell? Through filtering!

factors n = filter (\x -> mod n x == 0) [1..n]

main = do
  args <- getArgs              -- unwrap from the IO
  let arg0 = head arg          -- pull out just the first
  let n = read arg0 :: Int     -- parse the int
  putStrLn $ show $ factors n

We should remind ourselves why factors are important. Wasn’t it the Babylonians who chose to use 360 for the number of degrees in a circle because 360 had so many ways to divide it evenly? They made it far easier to split a pizza amongst groups of various sizes.

Let’s do another example, this time with a loop. Let’s print the first command-line parameter vertically. For acrostics and crossword puzzles and stuff. We still don’t have loops, but we can use recursion. Here’s our function that “loops”:

downer :: String -> IO ()
downer [] = return ()
downer (first:rest) = do
  putStrLn [first]
  downer rest

And our main:

main = do
  args <- getArgs
  let arg0 = head args
  downer arg0

One of your homework problems requires a loop until the user gets a right answer. We can’t use pattern matching for that, so let’s rewrite this using a more flexible construct:

downer list = do
  if list == [] then
    return ()
  else
    putStrLn [first]
    downer rest

Now, let’s do a comprehensive example of a full Haskell program with a real purpose: solving day 1 of Advent of Code 2016.

First up, we need a main that reads in the input file. The standard library can help immensely here:

import System.IO

main = do
  text <- readFile "day1.in"
  putStrLn text

Next let’s split up the list:

import System.IO
import Data.List.Split

main = do
  text <- readFile "day1.in"
  let commands = splitOn ", " text
  putStrLn $ show commands

Okay, we’ve got a list of commands of turning and moving. Each has this form: [RL]\d+. If the commands are L3, R1, R3, R1, that would play out as follows:

  1. We start off at what we’ll call (0, 0), facing north.
  2. We turn left to face west and move 3 units to (-3, 0).
  3. We turn right to face north and move 1 units to (-3, 1).
  4. We turn right to face east and move 3 units to (0, 1).
  5. We turn right to face south and move 1 unit to (0, 0).

It would be silly to fully walk that route, as we just end up back where we started. We don’t have time to waste! Instead we want to simulate the steps in a program and determine where the Easter Bunny Headquarters is. Let’s practice getting a feel for the problem. Where would we end up with the following commands?

Alright, let’s pick off small pieces of this problem at a time. Let’s model our current direction as a pair of Ints. What happens to our direction when we turn?

Current Direction After L After R
(0, 1) (-1, 0) (1, 0)
(1, 0) (0, 1) (0, -1)
(0, -1) (1, 0) (-1, 0)
(-1, 0) (0, -1) (0, 1)

Let’s write a function to compute our new direction based on the letter in a command. We don’t need a conditional statement for this, as the x and y components just sort of trade places, with some negation thrown in:

turn :: Char -> (Int, Int) -> (Int, Int)
turn 'L' (dx, dy) = (-dy, dx)
turn 'R' (dx, dy) = (dy, -dx)

Next we need that takes us from one location and direction to the next location and direction given a command. Let’s accept the state of our simulations as a 4-tuple:

processCommand :: (Int, Int, Int, Int) -> String -> (Int, Int, Int, Int)
processCommand (x, y, dx, dy) command = (x', y', dx', dy')
  where ...

Okay, so what’s our new state? Let’s start by extracting out the letter and number from the command:

processCommand (x, y, dx, dy) command = (x', y', dx', dy')
  where
    letter = head command
    number = read (tail command) :: Int

We can use turn to get our new direction:

processCommand (x, y, dx, dy) command = (x', y', dx', dy')
  where
    letter = head command
    number = read (tail command) :: Int
    (dx', dy') = turn letter (dx, dy)

Finally, then, we can use our computed direction to jump to the next location:

processCommand :: (Int, Int, Int, Int) -> String -> (Int, Int, Int, Int)
processCommand (x, y, dx, dy) command = (x', y', dx', dy')
  where
    letter = head command
    number = read (tail command) :: Int
    (dx', dy') = turn letter (dx, dy)
    (x', y') = (x + number * dx', y + number * dy')

We can test this!

processCommand (0, 0, 0, 1) "R10" -- should put us at (10, 0, 1, 0), right?

Now, time to process the whole sequence. That’s just a recursive solution, right?

processCommands :: (Int, Int, Int, Int) -> [String] -> (Int, Int, Int, Int)
processCommands state [] = state
processCommands state (first:rest) = processCommands (processCommand state first) rest

Hey, wait. What are we doing here? We’re reducing a list down to a single summary value. Scrap that last definition. This is a job for fold!

processCommands start = foldl' processCommand start

Finally, we can complete our main!

main = do
  text <- readFile "day1.in"
  let commands = splitOn ", " text
  let stop = processCommands (0, 0, 0, 1) commands
  putStrLn $ show stop

The problem actually wants us to compute how many blocks away is the headquarters:

main = do
  text <- readFile "day1.in"
  let commands = splitOn ", " text
  let (x, y, _, _) = processCommands (0, 0, 0, 1) commands
  putStrLn $ show $ abs x + abs y

See you next time, when we discuss laziness and tail recursion!

Sincerely,