Haskell from the ground up

Tuples, zip and paired data

Bundling a few values together when they belong together, walking two lists side by side with zip and zipWith, and reading the halves of a pair.

Part 7 of 12

Intermediate16 min readBack to Courses

A tuple is a fixed bundle

A tuple holds a fixed number of values, and the types may differ: ("Ada", 88) has the type (String, Int). A list is for many values of one type; a tuple is for a few values that belong together as one thing.

The number of values is part of the type. A pair and a triple are different types, and a function written for a pair will not accept a triple, which is exactly the kind of mistake worth catching early.

zip walks two lists together

zip takes two lists and makes a pair out of every matching position, so pairing a list of names with a list of marks gives a list of pairs. zipWith does the same walk but combines each pair with a function of your own.

zip stops as soon as the shorter list runs out, so lists of different lengths give you fewer pairs rather than an error. That is convenient, and it is also the kind of thing to check when a count comes back wrong.

Reading the halves

fst and snd read the two halves of a pair. A function can also match on the shape: line (name, mark) = ... names both halves in its argument, which is how a list of pairs becomes a report line by line.

This is where the printing map earns its place. It applies an action to every value in a list and keeps none of the results, which is why its name ends in an underscore: what matters is the printing, not the answers. One short line then turns a list of lines into a printed report.

Pairs, built and taken apart

pairs :: [(Int, Int)]
pairs = zip [1, 2, 3] [10, 20, 30]

addThem :: (Int, Int) -> Int
addThem (a, b) = a + b

totals :: [Int]
totals = zipWith (+) [1, 2] [10, 20]

Names and marks, zipped

Run it and read the seven lines. Then add a fourth name and mark to both lists, and work out what the bonus line would say before you scroll down to check.

Output
Click "Run" to execute your code

When the same bundle appears all over a program, give it a name with data instead of dragging a raw tuple around. A named type can carry its own printing and comparison, which a tuple cannot, and that is the subject of a later chapter.

One pair at a time

map passes one value at a time to the function it is given. When the list holds pairs, that value is a pair, so the function has to take one pair: line (name, mark) fits, and line name mark does not. zipWith is the version that takes the two lists itself and hands your function the values separately.

That is the whole difference between map addThem (zip xs ys) and zipWith addThem xs ys: the first builds a list of pairs and then takes it apart, the second never builds the pairs at all. Both answer the same list.

zip3 extends the idea to three lists, and its function takes a three-part tuple.

Prelude supplies the two conversions between the shapes. curry turns a function that takes one pair into a function that takes two arguments, and uncurry does the reverse: uncurry (+) (2, 3) is 5, and curry fst 2 3 is 2. They are worth knowing because a list of pairs and a two-argument function meet often, and one of the two is usually the shorter way to join them. zipWith is the built-in shortcut for the common case, so reach for the conversions when the pair already exists rather than when you are building one.

A tuple is not a list

A list can grow and shrink; a tuple cannot. (1, 2) and (1, 2, 3) are different types, and no function written for one accepts the other. That rigidity is the point: a pair of a name and a mark cannot accidentally lose its mark.

You cannot map over a tuple, because there is no single place to put the answer: mapping a pair with a function from numbers to text would produce two different types, which is fine only if the function says so in advance. Pattern matching names the parts, and the function decides what to do with each.

fst and snd are shortcuts for pairs only. For a triple, write the pattern yourself: total (a, b, c) = a + b + c keeps the field order visible in one place.

zip stops at the shorter list without a word, so a missing name or a missing mark produces a shorter report rather than an error; check the lengths when the count matters. fst and snd do not work on triples, because a triple has no second half in that sense. And a list of pairs is not a pair of lists: unzip converts one to the other, and printing the wrong one shows the difference at once.

Try these next

  • Zip three lists of your own with zip3 and print the tuples without a helper function.
  • Take unzip of the pairs, then rebuild them with zip and check the two lists are unchanged.
  • Write swap :: (a, b) -> (b, a) and use it on one pair.

Three lists in, two lists out

Run it and read the five lines. Then add a fourth name, quantity and price, and work out the total before you run it again.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What is the type of ("Ada", 88)?