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.
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)
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.