Haskell from the ground up

List comprehensions

Describing the list you want instead of the steps that build it: a generator says where the values come from, and a condition keeps some of them.

Part 8 of 12

Intermediate16 min readBack to Courses

Describe the list you want

A comprehension writes the shape of the answer instead of the loop that produces it. [n | n <- [1 .. 6]] is nothing more than the numbers 1 to 6 written out; put an expression in front of the bar and it becomes the value you want for every n, which is how a list of squares fits on one short line.

The arrow is read as taken from. The name on its left is a fresh name you choose, and [1 .. 6] is the range from 1 to 6 inclusive, which is the shortest way to write a list of consecutive whole numbers.

A condition keeps some of them

A comma after a generator adds a condition, and only the values that satisfy it are used. [n | n <- [1 .. 12], mod n 2 == 0] takes the numbers from 1 to 12 and keeps the even ones.

The condition is an ordinary expression that has to be a Bool, so it can use comparisons, mod for remainders and div for whole-number division. Several conditions can follow one generator, which reads like a list of requirements.

Two generators, every combination

Two generators give every combination of the two: for each value of the first, the second walks its whole list. The left generator is the slower one, so it changes last, exactly as a loop written inside another loop would behave.

Combinations grow quickly, so a condition is usually part of the plan: a < b throws away the repeats where the two values are the same or swapped, which is how a list of pairs without duplicates is built.

Four comprehensions

wordLengths :: [Int]
wordLengths = [length w | w <- ["tea", "fig"]]

tens :: [Int]
tens = [div m 10 | m <- [34, 71, 9]]

grid :: [(Int, Int)]
grid = [(r, c) | r <- [1, 2], c <- [1, 2]]

Squares, evens and pairs

Run it and check the evens line against the range. Then write a comprehension that keeps the squares above 10, and one that makes pairs where both values are the same.

Output
Click "Run" to execute your code

The same shape works on text, because text is a list: [c | c <- "abc"] is the text abc again, and you can rebuild it however you like. A comprehension is not a special syntax for numbers, it is a way of describing any list.

A comprehension is a map and a filter

[double n | n <- xs] answers exactly what map double xs answers. The generator supplies the values, and the expression before the bar is applied to each one. Adding a condition is a filter: [n | n <- xs, even n] keeps what filter even xs keeps.

Two generators are two nested walks. [(a, b) | a <- xs, b <- ys] fixes an a, runs through all of ys, then moves to the next a, which is the order a loop inside a loop would produce. It is not zip, and the difference in length is the quickest way to tell them apart.

The expression decides the type

Whatever sits before the bar is the element type of the answer. Put n * n there and you have a list of numbers; put (n, n * n) there and you have a list of pairs; put show n there and you have a list of text.

Because a String is a list of Char, a comprehension over text that produces characters builds text: [c | c <- word, c /= ' '] removes the spaces. Nothing special is needed, and the same expression with show c would build a list of quoted characters instead.

A condition is an ordinary Bool expression evaluated for each candidate, so it can call a helper you wrote. Naming that helper makes the comprehension read as a sentence.

A generator can be followed by a let, which names a value for the rest of the comprehension without producing a candidate of its own. [(n, half) | n <- [2, 4, 6], let half = div n 2] walks three numbers and makes three pairs; the let is not a filter and not a second list. It is the same let as in a do block, used to keep an expression out of the way.

That matters when the same expression is needed twice: in the value before the bar and in a condition after it. Naming it once means the two uses cannot drift apart, and the comprehension reads as a sentence with a definition in the middle.

A condition has to answer a Bool, so [n | n <- [1 .. 5], n] is refused, because a number is not a test. The order of the generators is the order of the loops, so swapping them reorders the answer even though it holds the same values. And two generators make every combination rather than pairs: [a + b | a <- [1, 2], b <- [10, 20]] has four values, while zipWith (+) [1, 2] [10, 20] has two.

Try these next

  • Write withoutVowels as a comprehension and test it on a word that contains every vowel.
  • Build a list of (n, n * n) for the odd numbers below 10, then print only the squares.
  • Rewrite pairs with zip and compare the two answers; they are not the same list.

Letters in, letters out

Run it and read the five lines. Then change `onlyVowels` so it answers the letters that are not vowels, and compare it with the second line.

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 does a comma after a generator do?