More list manipulation #
We’ll start off doing a bit more list manipulation, looking at some list comprehensions and pattern matching. Then we’ll implement merge sort.
List comprehensions & pattern matching #
For this section, the template code is code/lists-exercise3.hs
.
Let’s first look at some pattern matching, and combination with guard expressions.
Exercise
Write a functioncompress :: Eq a => [a] -> [a]
that eliminates consecutive duplicate elements of a list, but otherwise leaves the order unchanged.
Now some simple list comprehensions. A pythagorean triple is a tuple $(x, y, z)$ of positive integers where $x^2 + y^2 = z^2$. For our first go, we won’t care about ordering, so we’ll allow generation of both $(3, 4, 5)$ and $(4, 3, 5)$ (for example).
Exercise
Using a list comprehension, define
pyths :: Int -> [(Int, Int, Int)]
that generates all pythogorean truples with components less than or equal to the specified integer.For example
Prelude> pyths 5 [(3, 4, 5), (4, 3, 5)]
Note that integer exponentiation in Haskell is writtenx^y
.
Exercise
Now modify your answer to only produce unique triples (don’t worry about ordering), so for example
Prelude> pyths' 5 [(3, 4, 5)]
Recall that when introducing variables in a list comprehension, later generators can refer to the variables introduced by earlier ones.
Question
The power two is special here, indeed Fermat’s Last Theorem, proved by Andrew Wiles in 1994, states that the equation $x^n + y^n = z^n$ has no solutions for positive integers $x, y, z, n$ when $n > 2$.
You could try and confirm this for $n = 3$ by checking that the list generated by a comprehension with $x^3 + y^3 = z^3$ is empty.
Would you be able to use this to prove the theorem? If not, why not?
More than one way to do it #
Unlike Python for which the zen of python says that
There should be one – and preferably only one – obvious way to do it.
In Haskell there are often multiple different ways to do the same thing, each of which may be more or less obvious depending on what you’re used to.
Let’s look at this by definining a function to compute the scalar product of two vectors (represented as lists) of numbers of length $n$. Recall that the scalar (or dot) product is defined as
$$ a \cdot b = \sum_{i=1}^{N} a_i b_i $$
Exercise
Define a function to compute the scalar product in three different ways
- Using
sum
and a list comprehension- Using
sum
,map
, andzip
.- Using
sum
andzipWith
The template code has a few more explanatory comments.
You may assume that both vectors have the same length.
Question
Which do you prefer, and why?
Solutions #
I’ve added some commented solutions to these exercises. If you have queries about them please ask in the practical sessions or else get in touch.
Merge sort #
Now we’re going to implement sorting of lists using the merge sort algorithm. Merge sort is a divide-and-conquer algorithm that is built out of three parts.
- Dividing a list into two sublists that are to be sorted.
- Sorting the sublists
- Merging two sublists that are already sorted
This has a very natural recursive definition and a succint Haskell implementation.
We’ll break this into parts. The template code for this exercise is in
code/lists-mergesort.hs
First, define a function
halve :: [a] -> ([a], [a])
which splits a list into two halves at its midpoint (for odd-length
lists the two sublists should have lengths that differ by at most
one). You might find splitAt :: Int -> [a] -> ([a], [a])
helpful.
Next define a function
merge :: Ord a => [a] -> [a] -> [a]
which takes two (sorted) lists and merges them into one sorted list. This is probably easiest to write recursively, think about the possible cases.
Finally use your two helper functions to write
mergeSort :: Ord a => [a] -> [a]
Again, the recursive definition is a natural one. There are base cases for empty and singleton lists, while the recursive case should implement the divide and conquer, splitting and merging sorted sublists.
A higher-order version #
Notice how this implementation of mergeSort
requires that the list
entries are
orderable.
It is often useful to instead allow the caller to provide a comparison
function that orders elements.
Rework your code to implement
mergeSortWith :: (a -> a -> Ordering) -> [a] -> [a]
Where the
Ordering
type has three values LT
, EQ
, or GT
.
You’ll need to implement new mergeWith
and mergeSortWith
functions.
With mergeSortWith
implemented, the implementation of mergeSort
is
then just
mergeSort' :: Ord a => [a] -> [a]
mergeSort' = mergeSortWith compare
Where we used the generic function compare.
Question
Do you understand how the implementation ofmergeSort'
works using currying and partial application?
Composition #
We can use this idea of higher-order functions and composition as building blocks for a number of different ways of sorting objects.
To simplify spelling things, let’s introduce a type-synonym for the type of the comparison function
type Comparator a = a -> a -> Ordering
We can now do various things, suppose that we want to sort lists in a reverse order from the one given by the default ordering. We could do
sortReverse :: Ord a => [a] -> [a]
sortReverse = reverse . mergeSortWith compare
But this unnecessarily traverses the list to reverse it at the end.
Better is to invert the comparison function. I provide an
invertOrdering
function that reverses an ordering.
Exercise
Implement
invert :: Comparator a -> Comparator a
Which takes a comparator and produces a new comparator that delivers the reverse. We can then implement
sortReverse
without the extra traversalsortReverse :: Ord a => [a] -> [a] sortReverse xs = mergeSortWith (invert compare) xs
The
invert
function returns a function, so we need a way to introduce names for its arguments (they don’t appear on the left hand side of the definition).The easiest way to do this is by using a lambda expression on the right hand side. If you want to then see how write it in pointfree style, go to pointfree.io.
Now let’s look at slightly more involved comparator transformations.
Suppose we have a Comparator a
and a way of turning b
s into a
s,
we can use this to deliver a Comparator b
. We’ll call this function
on
for reasons which will become (hopefully) obvious.
on :: Comparator a -> (b -> a) -> Comparator b
on = undefined
For example, suppose we want to compare tuples by their first element
using the builtin compare
on that element.
Our comparison operation would be
on compare fst
Now we can see why we called this function on
, since when we write
it with infix notation
compare `on` fst
We read this as “Compare the elements on their first part”. As an example, if we run
Prelude> mergeSortWith (compare `on` fst`) [(7, "a"), (-1, "b"), (5, "d")]
[(-1, "b"), (5, "d"), (7, "a")]
Exercise
Implement
on
. Check that it produces the right answer.Like with
invert
, theon
function returns a function, so we need a way to introduce names for its arguments (they don’t appear on the left hand side of the definition).The easiest way to do this is by using a lambda expression on the right hand side. If you want to then see how write it in pointfree style, go to pointfree.io.
If the transformation function you provide is the identity function
id
, then you should get back the same thing as if you had not usedon
. This is a useful sanity check.That is
compare `on` id == compare
Exercise
Finally, we’ll compose these two extra functions. Write a function
sortReversedByLengthSnd :: [(a, String)] -> [(a, String)]
That sorts a list of pairs of something and strings in reverse order by the length of the string.
For example
Prelude> sortReversedByLengthSnd [(5, "a"), (2, "foo"), (3, "four"), (7, "ab")] [(3, "four"), (2, "foo"), (7, "ab"), (5, "a")]
You should not write this comparison function by hand, but just compose together functions which you already have.
Solutions
I’ve added some commented solutions to these exercises. If you have queries about them please ask in the practical sessions or else get in touch.