Mapping

If you’ve used only one of Python’s iterator functions, there’s a good chance it’s this one. map in Python

map is essential because it’s the functor operation for lists: The notion of a functor is a small thing, but it provides a surprising depth of useful ideas to think about. We go into functors in depth in Functortown: A Map of the Territory. It operates over the content of the list but not the structure of the list. This implies, among other things, that the list that map returns will always have the same number of elements as the input list.

map

>>> xs = [1, 2, 3]

>>> it = map(lambda x: x * 10, xs)

>>> list(it)
[10, 20, 30]

This function has the same name in Haskell. map in Data.List and Prelude

λ> xs = [1, 2, 3]

λ> map (\x -> x * 10) xs
[10,20,30]

You often see map written in Haskell in its more polymorphic form, fmap. fmap in Data.Functor and Prelude This is a function that works on any type that has a Functor instance. For lists, it specializes to map. So this is the same as the previous example:

λ> fmap (\x -> x * 10) xs
[10,20,30]

There is also an infix version of fmap, called (<$>). So we could also write this example as:

λ> (\x -> x * 10) <$> xs
[10,20,30]

Whether you use fmap or (<$>) is up to you – sometimes one or the other will be more convenient or clear in a particular context, but often it’s just a matter of personal preference.

With more than one iterator

When map is applied to a binary function and two list arguments, it does something a bit more: It steps through both lists in tandem, applying the function to one element from each of the lists.

>>> it = map(operator.add, [1, 2, 3], [10, 100, 1000])

>>> list(it)
[11, 102, 1003]

This notion of iterating through two lists at once is sometimes called zipping. We will discuss zipping more when we get to the zip function in the next lesson. Haskell calls this function zipWith: zipWith in Data.List and Prelude

λ> zipWith (+) [1, 2, 3] [10, 100, 1000]
[11,102,1003]

Python’s map can be used with any number of arguments. Haskell does not overload like this, but has separate functions zipWith3, zipWith4, etc. to achieve the same result.

zipWith  :: (a -> b -> c)           -> [a] -> [b] -> [c]
zipWith3 :: (a -> b -> c -> d)      -> [a] -> [b] -> [c] -> [d]
zipWith4 :: (a -> b -> c -> d -> e) -> [a] -> [b] -> [c] -> [d] -> [e]

starmap

starmap itertools.starmap is like a cross between map used with a single iterator and map used with multiple iterators. It only takes a single iterator parameter, but the function being mapped doesn’t have to be unary. Each element of the iterator is a tuple containing as many arguments as the function has parameters.

>>> it = starmap(pow, [(2,5), (3,2), (10,3)])

>>> list(it)
[32, 9, 1000]

In this example the function we mapped (pow) has two parameters, and so each element of the iterator is a 2-tuple.

This is called “star map” because This use of * in Python is called iterable unpacking. we could have also written it like this:

>>> it = map(lambda x: pow(*x), [(2,5), (3,2), (10,3)])

>>> list(it)
[32, 9, 1000]

There’s no precise Haskell analogue here because Python generalizes over function arity in a way that Haskell doesn’t. If we wanted to compute exponents over a list of number pairs as the example above does, we’d probably write something like this:

λ> map (\(a, b) -> a ^ b) [(2,5), (3,2), (10,3)]
[32,9,1000]

For functions of two parameters, Python’s “star” is reminiscent of uncurrying in Haskell. uncurry in Data.Tuple and Prelude If we wrote a function similar to starmap, we might call it uncurrymap:

uncurrymap = map . uncurry

For a function of two parameters, we can use this just like starmap.

λ> uncurrymap (^) [(2,5), (3,2), (10,3)]
[32,9,1000]

We mention uncurry here mostly as a curiosity. It does not appear often in Haskell code, and the first version using map and a lambda expression is the more common way to write this expression.

Join Type Classes for courses and projects to get you started and make you an expert in FP with Haskell.