You can read about these subjects in Learn You a Haskell, chapters 8 and 9.
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
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 the next lecture we'll learn more about the type classes underlying 'do' blocks, and will learn how to make our own types that work with 'do'.