Week 11: Notes

input and output
I/O actions
files and streams

You can read about these topics in Learn You a Haskell, chapter 9 "Input and Output".

foldables

We've previously seen that the Functor type class represents any type that we can map over using fmap. Similarly, the Foldable type class represents any type that we can fold over. In fact the built-in functions foldl and foldr are members of this type class.

Certainly the built-in list type [] is an instance of Foldable. In fact Maybe is also a Foldable:

> foldl (+) 2 (Just 10)
12
> foldl (+) 2 Nothing
2

We can see that when folding, a Just acts like a single-element list, and Nothing acts like an empty list.

Many of the built-in functions that we've seen on lists will actually work on any Foldable. These include all, any, elem, product, sum, and others. So we can write, for example:

> any (>3) (Just 5)
True
> any (>3) Nothing
False
> all (>3) (Just 5)
True
> all (>3) Nothing
True
> sum (Just 5)
5
> sum Nothing
0

We may declare our own types to be instance of Foldable. To do so, we only need to implement foldr. For example, let's define a binary tree type and make it foldable:

data Tree a = Nil | Node (Tree a) a (Tree a)
  deriving (Show, Read)

instance Foldable Tree where
  foldr f a Nil = a
  foldr f a (Node left x right) =
    foldr f (f x (foldr f a right)) left

Because our type is Foldable, many useful built-in functions will now work on it automatically:

> my_tree = Node (Node (Node Nil 1 Nil) 2 (Node Nil 3 Nil)) 4 (Node Nil 5 Nil)
> sum my_tree
15
> elem 3 my_tree
True
> all (>0) my_tree
True

In fact Haskell will even implement foldl automatically given our implementation of foldr:

> foldl (+) 0 my_tree
15

arrays

Haskell has immutable arrays that allow you to access elements in O(1) using the ! operator. You'll need to import Data.Array to use these.

The listArray function builds an array given a range of indices and a list of values. Let's build an array whose indices are the values 0..5:

> a = listArray (0, 5) [2, 9, 8, 3, 7, 6]

Now we can access elements in constant time:

> a ! 2
8
> a ! 4
7

Array indices can be any instance of the Ix type class. That includes Int, Integer, Char, and also tuples of types belonging to Ix. So we can construct, for example, a 2-dimensional array whose indices are of type (Int, Int):

> a = listArray ((0, 0), (2, 2)) [7, 2, 4, 8, 9, 5, 3, 11, 14]

This represents the matrix

1  2  4
8  9  5
3  11 14

Again, we can access elements in constant time:

> a ! (1, 1)
9
> a ! (1, 2)
5

The built-in function accumArray builds an array from an association list, using a function to fold together all values with the same key. It has this type:

accumArray :: Ix i => (e -> a -> e) -> e -> (i, i) -> [(i, a)] -> Array i e

Here, i is the index type, e is the type of the elements in the input list, and 'a' is the type of the accumulator used for joining elements. This function can be useful for building a histogram of values in a fixed range. As an example, this function will sort a list of integers in a fixed range in linear time:

-- counting sort
sort :: Int -> Int -> [Int] -> [Int]
sort lo hi xs =
    let arr = accumArray (+) 0 (lo, hi) [(x, 1) | x <- xs]
    in concat [replicate x i | (i, x) <- assocs arr]

Array elements are evaluated lazily, and may even refer to other elements of the same array:

> a = listArray (1, 3) [10, 20, a ! 1 + a ! 2]
> a ! 1
10
> a ! 3
30

So we can build an entire array using a recursive calculation. We can use this technique to solve dynamic programming problems in a top-down fashion. For example, consider the following function that calculates the Nth Fibonacci number recursively. It is exponentially inefficient:

fib :: Int -> Int
fib 1 = 1
fib 2 = 1
fib n = fib (n - 1) + fib (n - 2)

We can use dynamic programming to cache each return value in an array:

fib :: Int -> Int
fib n = a ! n
    where a = listArray (1, n) (1 : 1 : map f [3..n])
          f n = a ! (n - 1) + a ! (n - 2)

Now the function will run in linear time.

'do'

In Haskell, a do block is like a generalized list comprehension.

As a first example, we may use 'do' with ordinary lists. Consider this list comprehension:

> list = [(x, y) | x <- [1..2], y <- [1..3]]
> list
[(1,1),(1,2),(1,3),(2,1),(2,2),(2,3)]

We may alternatively write it using 'do' notation:

list = do
    x <- [1..2]
    y <- [1..3]
    return (x, y)

We see that in a 'do' block, we can use the <- operator to retrieve a value. At the end of the 'do' block, we can use the 'return' function to produce a value to be returned. When we use 'do' with ordinary lists, we retrieve values from lists, and the return values are joined together into a list.

We may also use 'do' with Maybe. Here's a function that takes three values of type (Maybe Int), and returns their sum:

add :: Maybe Int -> Maybe Int -> Maybe Int -> Maybe Int
add mx my mz = do
    x <- mx
    y <- my
    z <- mz
    return (x + y + z)

Let's try it:

> add (Just 3) (Just 4) (Just 5)
Just 12
> add (Just 3) Nothing (Just 5)
Nothing

Notice that if any of the input values is absent, the result is Nothing. To see why this is true, recall that a Maybe behaves like a list that has 0 or 1 elements, and notice that the following 'do' block will produce an empty list:

list = do
    x <- [3]
    y <- []
    z <- [5]
    return (x + y + z)

A 'do' block with Maybe values can be quite useful for error handling. Suppose that we want to call several functions, any of which might fail and return Nothing. If we place these calls in a 'do' block, then it will return Nothing if any of the function calls failed.

As we saw in the lecture (and you can also read about in Learn You a Haskell), 'do' blocks also work with I/O objects. In future lectures we'll learn more about the type classes underlying 'do' blocks, and will learn how to make our own types that work with 'do'.