map
Remember our good old map function. It allows us to transform the elements in
a list by applying a function to each list element. The resulting list can have
a different type than the input list, if the function we apply to each list
element has different argument and return types. For example,
>>> :t show
show :: Show a => a -> String
>>> map show [1,2,3,4,5]
["1","2","3","4","5"]
Since show turns its argument into a string, mapping show over a list of
integers turns this list into a list of strings.
Here's the implementation of map:
map :: (a -> b) -> [a] -> [b]
map f = go
where
go (x:xs) = f x : go xs
go _ = []
As we will learn when talking about functors, map is in fact merely the
list-specific version of a more general function called fmap. fmap works for
arbitrary containers, such as arrays, binary trees, etc.
Exercise
The standard library has a function
replicate :: Int -> a -> [a]
replicate n x creates a list of n copies of x:
>>> replicate 3 1
[1,1,1]
>>> replicate 10 100
[100,100,100,100,100,100,100,100,100,100]
Implement this function. You should try to implement it manually, without
the help of any functions in the standard library, and using map. For the
implementation using map, you need two tools. First, the list
comprehension [1..n] generates the list of all numbers between 1 and \(n\):
>>> [1..10]
[1,2,3,4,5,6,7,8,9,10]
Second, there's a function we will use later when talking about functors and monads:
const :: a -> b -> a
const x _ = x
This function simply ignores its second argument and returns its first
argument. It's more illuminating to partially apply it. const 5, for
example, is a function that ignores its argument and always returns 5. Hence
the name: it's the constant function.
Solution
Here's the manual implementation:
replicate :: Int -> a -> [a]
replicate n x = go n
where
go 0 = []
go m = x : go (m - 1)
The implementation using map uses the following idea: We generate a
list of length n. It doesn't matter what it contains, only that it has
length n. An easy way to generate a list of length n is to use the
list comprehension [1..n]. Now we use map to replace every element
in this list with x. That gives us a list of n copies of x. The
function we use to replace every element in [1..n] with x is
const x. This gives the following implementation:
replicate :: Int -> a -> [a]
replicate n x = map (const x) [1..n]
Exercise
map allows us to apply a function to every list element and collect the
results in a list. This works even if the function we apply returns a list.
In that case, the result is a list of lists:
>>> map (\x -> replicate x x) [1..5]
[[1],[2,2],[3,3,3],[4,4,4,4],[5,5,5,5,5]]
It is quite common that we don't want a list of lists as the result but that
we want the concatenation of the lists produced by applying our function to
each list element. We can do this by applying concat to the result of
map:
>>> concat $ map (\x -> replicate x x) [1..5]
[1,2,2,3,3,3,4,4,4,4,5,5,5,5,5]
Since this is a common thing to do, the standard library has a function that
combines concat and map:
concatMap :: (a -> [b]) -> [a] -> [b]
It behaves exactly as if it were defined as
concatMap f xs = concat $ map f xs
>>> concatMap (\x -> replicate x x) [1..5]
[1,2,2,3,3,3,4,4,4,4,5,5,5,5,5]
However, in the interest of efficiency, concatMap is implemented directly,
without the help of concat or map. Provide your own implementation of
concatMap without using concat or map.
Solution
concatMap :: (a -> [b]) -> [a] -> [b]
concatMap f = go
where
go [] = []
go (x:xs) = f x ++ go xs