Lecture 07 List Comprehensions

Joseph Haugh

University of New Mexico

Objectives

By the end of this lesson, you will know how to:

  • Use basic list comprehensions
  • Use list comprehensions with multiple generators
  • Use list comprehensions with guards
  • Use the zip function
  • Use list comprehensions with Strings
  • Understand how to implement the caesar cipher

Set Notation

What does the following mathematical expression mean?

{ x | x ∈ {1..5} }

The set of all x such that x is an element of the set {1, 2, 3, 4, 5}.

Basic List Comprehensions

We can do this same thing in Haskell using list comprehensions:

[ x | x <- [1..5] ]

The list of all x such that x is an element of the list [1, 2, 3, 4, 5].

List Comprehensions

What does this produce?

[ x ^ 2 | x <- [1..5] ]

The list of all x^2 such that x is an element of the list [1, 2, 3, 4, 5].

[1, 4, 9, 16, 25]

List Comprehensions

What does this produce?

[ (x, x) | x <- [1..5] ]
[(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)]

Multiple Generators

You can also use multiple generators in a list comprehension:

[ (x, y) | x <- [1, 2, 3], y <- [4, 5] ]
[(1, 4), (1, 5), (2, 4), (2, 5), (3, 4), (3, 5)]

This is called a Cartesian Product

Multiple Generators

What happens if we flip the order of the generators? Note that the names of the lists and the order of the tuple remain unchanged.

[ (x, y) | y <- [4, 5], x <- [1, 2, 3] ]
[(1, 4), (2, 4), (3, 4), (1, 5), (2, 5), (3, 5)]

Dependent Generators

You can also use the value of a previous generator in a later generator:

[ (x, y) | x <- [1..3], y <- [x..3] ]
[(1, 1), (1, 2), (1, 3), (2, 2), (2, 3), (3, 3)]

Example: seconds

How could you take in a list of tuples and return a list of the second elements of each tuple?

What would the type of that function be?

seconds :: [(Int, Int)] -> [Int]

Can we be more general?

seconds :: [(a, a)] -> [a]
seconds :: [(a, a)] -> [a]
seconds xs = [ y | (_, y) <- xs ]

Example: length

How could you take in a list and returns its length using a list comprehension and the sum function?

What should the type be?

length :: [a] -> Int
length :: [a] -> Int
length xs = sum [ 1 | _ <- xs ]

Guards

What about if we only want to take in a list and only return the even numbers squared?

  • We can do this using guards
  • Guards in this context filter out values that don’t meet the condition

Here is how we can implement that function:

evensSquared :: [Int] -> [Int]
evensSquared xs = [ x ^ 2 | x <- xs, even x ]

Example: factors

How could you take in a number and return a list of its factors?

factors :: Int -> [Int]
factors n = [ x | x <- [1..n], n `mod` x == 0 ]

Example: prime

Using factors, how could you implement a function to check if a number is prime?

prime :: Int -> Bool
prime n = factors n == [1, n]

zip Function

The zip function takes two lists and returns a list of tuples where the first element of each tuple is from the first list and the second element is from the second list.

For example:

> zip [1, 2, 3] [4, 5, 6]
[(1, 4), (2, 5), (3, 6)]

Example: pairs

A useful technique is to use the zip function to create a list of pairs of adjacent elements from a list.

pairs :: [a] -> [(a, a)]
pairs xs = zip xs (tail xs)
> pairs [1, 2, 3, 4]
| zip [1, 2, 3, 4] (tail [1, 2, 3, 4])
| zip [1, 2, 3, 4] [2, 3, 4]
| [(1, 2), (2, 3), (3, 4)]

zip Behavior

What happens when the lists given to zip are of different lengths?

As you can glean from the previous example, zip stops when the shorter list ends.

> zip [1, 2, 3] [4, 5]
[(1, 4), (2, 5)]

> zip [1, 2] [4, 5, 6]
[(1, 4), (2, 5)]

String Comprehensions

Recall that a String in Haskell is [Char]

This means that we can use list comprehensions to manipulate Strings

count :: Char -> String -> Int
count c s = length [ x | x <- s, x == c ]
> count 's' "mississippi"
4

Example: caesar

Julius Caesar would encode his messages by shifting each letter by 3 to the right, and wrapping around to the beginning if necessary.

Example: caesar

We will need to import a library to manipulate Chars in order to implement the caesar cipher

import Data.Char

This library includes helpful functions like ord and chr

  • ord takes a Char and returns its ASCII value
  • chr takes an ASCII value and returns the corresponding Char

Example: caesar

First let’s implement two functions to convert between Chars and their ASCII values, but make it so that lowercase ‘a’ is 0, ‘b’ is 1, and so on.

let2int :: Char -> Int
let2int c = ord c - ord 'a'

int2let :: Int -> Char
int2let n = chr (ord 'a' + n)
> let2int 'a'
0
> int2let 0
'a'

Example: caesar

Now we can implement a function to shift a lower case letter by n places to the left or right

shift :: Int -> Char -> Char
shift n c
    | isLower c = int2let ((let2int c + n) `mod` 26)
    | otherwise = c

Note this only works on lower case letters.

> shift 3 'a'
'd'
> shift 3 'z'
'c'
> shift (-3) 'c'
'z'
> shift 3 'A'
'A'

Example: caesar

Now we can easily define a function which encodes a given String using the caesar cipher method

encode :: Int -> String -> String
encode n xs = [ shift n x | x <- xs ]
> encode 3 "haskell"
"kdvnhoo"
> enocde (-3) "kdvnhoo"
"haskell"

Exercise: Sum of Squares

Use a list comprehension to implement a function which calculates the sum of the first n squares

Recall that the sum function can be used to sum a list of numbers.

For example:

> sumSquares 5
| sum [ 1^2, 2^2, 3^2, 4^2, 5^2 ]
| 55

sumSquares :: Int -> Int

Exercise: Sum of Squares

sumSquares :: Int -> Int
sumSquares n = sum [ x ^ 2 | x <- [1..n] ]

Exercise: Replicate

Use a list comprehension to implement the replicate' function, this function is given an Int n and a value x and returns a list of n xs.

Note the ' in the name to avoid conflicting with the built-in definition.

For example:

> replicate' 3 5
[5, 5, 5]

replicate' :: Int -> a -> [a]

Exercise: Replicate

replicate' :: Int -> a -> [a]
replicate' n x = [ x | _ <- [1..n] ]