unfoldr
zip
allows us to zip two lists together. unzip
is its inverse: it takes a
list of pairs and splits it into two lists containing the first and second
elements of the pairs in the input list. Similarly, unfoldr
can be viewed as
the inverse of foldr
: Where foldr
reduces a list down to a single value,
unfoldr
can be used to produce a list from a seed value. The goal is to
produce a list of b
s, a list of type [b]
, from a seed value of type a
. To
do this, we need a generator function. This generator function needs to do three
things:
- Given the initial seed value, it needs to produce the first list element.
- It also needs to produce an updated seed value from which to produce the remainder of the list.
- It needs to tell us when the whole list has been generated, when we're done.
A natural type for such a generator function is a -> Maybe (b, a)
. If there
are no more list elements to be produced, the result is Nothing
. Otherwise,
the result is Just
a pair consisting of the first list element and the updated
seed value from which to produce the rest of the list. For example,
>>> import Data.List
>>> gen x = if x > 10 then Nothing else Just (x, x+1)
>>> unfoldr gen 0
[0,1,2,3,4,5,6,7,8,9,10]
The initial seed value is 0. Given a seed value of x
, gen
produces x
as
the next list element and chooses x+1
as the next seed value. It does this
until x > 10
, at which point it returns Nothing
to indicate that the list is
done. unfoldr gen 0
thus generates the list [0,1,2,3,4,5,6,7,8,9,10]
. What
would unfoldr gen 5
generate? Formulate an answer first and then try it out in
GHCi.
Here's how unfoldr
is implemented:
unfoldr :: (a -> Maybe (b, a)) -> a -> [b]
unfoldr gen = go
where
go seed = case gen seed of
Just (x, seed') -> x : go seed'
_ -> []
Exercise
In an earlier exercise, I asked you to implement replicate
.
replicate n x
produces a list containing n
copies of x
:
>>> replicate 5 1
[1,1,1,1,1]
>>> replicate 10 1
[1,1,1,1,1,1,1,1,1,1]
Implement this function using unfoldr
.
Solution
replicate n x = unfoldr rep n
where
rep 0 = Nothing
rep m = Just (x, m - 1)
Remember, the logic of unfoldr gen seed
is to generate list elements
from left to right by calling gen
repeatedly on the current seed. If
gen
returns Nothing
, we're done; there are no more list elements to
add. If gen
returns Just (x, seed')
, then x
becomes the next list
element and seed'
is the new seed value from which to generate the
elements following x
.
Here, we use n
as our seed. If the seed is 0, then we do not want to
generate any list elements, so rep 0 = Nothing
. If the seed is
positive, then we add one copy of x
to the list and decrease our seed
by 1 to indicate that we have one less copy of x
to add.
Exercise
The list comprehension [x..y]
generates the list of all numbers between x
and y
:
>>> [3..8]
[3,4,5,6,7,8]
Implement a function genRange x y
that generates the same list. Do not use
list comprehensions. Use unfoldr
to implement genRange
instead.
>>> genRange 3 8
[3,4,5,6,7,8]
Solution
genRange x y = unfoldr gen x
where
gen z | z > y = Nothing
| otherwise = Just (z, z + 1)
Think about the seed value z
as generating the list [z..y]
. Then
unfoldr gen x
generates exactly the list [x..y]
. If z > y
, this
list is empty, so gen z
returns Nothing
. Otherwise, the first
element of [z..y]
is z
and the remainder of this list is the list
[z+1..y]
, to be generated from the seed z + 1
. Thus, gen z
returns
Just (z, z + 1)
.
Exercise
In an earlier exercise, I asked you to implement a function everyOther
that returns a list containing every other element in the input list:
>>> everyOther [1,2,3,4,5]
[1,3,5]
>>> everyOther "Hello"
"Hlo"
I asked you to implement this function using zip
, filter
, and map
.
Implement this function using unfoldr
instead. Think about how you can do
this by taking the input list as the seed. The point of this exercise is to
illustrate that the seed value from which to generate the result of
unfoldr
can have any type. It can even be a list itself.
Solution
everyOther :: [a] -> [a]
everyOther = unfoldr takeOneDropOne
where
takeOneDropOne (x:xs) = Just (x, drop 1 xs)
takeOneDropOne _ = Nothing
everyOther xs
uses xs
itself as the seed value. As long as the seed
list is not empty, we split it into its head x
and tail xs
. x
becomes the first element in the list generated from this seed. The next
element in the seed list should be dropped, so the new seed becomes
drop 1 xs
. This process continues until the seed list is empty. At
this point, we have no more elements to add to the output, so
takeOneDropOne
returns Nothing
.
You may wonder why the first equation of takeOneDropOne
isn't
takeOneDropOne (x:xs) = Just (x, tail xs)
. Think about what would
happen when calling takeOneDropOne [1]
. In this case, we would have
x = 1
and xs = []
. tail xs
would then panic because we cannot take
the tail of an empty list. drop 1 xs
returns the tail of xs
if xs
is non-empty, and the empty list if xs
is empty. Thus, drop 1 xs
behaves like a safe version of tail
here.