Week 13: Notes

records
input and output
I/O actions
files and streams
command-line programs

You can read about these subjects in Learn You a Haskell, chapters 8 and 9.

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. As one possible approach, we can provide an implementation of 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

There is another easier, approach that will work for algebraic data types such as Tree. We can simply derive Foldable, and then Haskell will implement both foldr and foldl automatically:

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

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 fact do blocks will work with any instance of a type class called Monad, which plays a fundamental role in advanced Haskell programming. [], Maybe and IO are all instances of Monad. In the next lecture we'll learn more about the Monad type class, and will learn how to make our own types that work with do.