Module 03.1: Lists, Tuples, Pattern Matching, and Parameterized Types¶
So far, we have mostly computed with individual values: numbers, Booleans, strings, and functions. Now we will start working seriously with structured data.
This module focuses on four ideas that fit together especially well in Haskell:
- lists, for collecting any number of values of the same type,
- functions on lists, especially recursive functions,
map, andfilter, - pattern matching, for taking structured values apart according to their shape,
- tuples, for grouping a fixed number of values that may have different types, and
- parameterized types, which let us describe general patterns like "a list of some type."
There is a practical reason to learn all of this: lists are everywhere in Haskell. There is also a deeper reason. Lists are recursive data structures, and functional programming makes heavy use of recursion. Add pattern matching and higher-order functions, and we get a compact way to express computations that would often be written as loops and mutable accumulators in an imperative language.
By the end of this module, you should be able to:
- construct and inspect Haskell lists,
- explain why Haskell lists are homogeneous,
- distinguish
:from++, - use common list functions such as
length,head,tail,take,drop,null, andelem, - explain how
mapandfilterabstract recurring patterns of list computation, - pattern-match on lists using
caseexpressions and function definitions, - recognize empty-list and nonempty-list cases as the natural structure of recursive list functions,
- construct and pattern-match on tuples,
- read type variables such as
aandbin list-function types, and - explain what it means for a type such as
[a]to be parameterized.
How to use this module
When you see a box labeled Gradescope question, answer that question in the Module 3.1 Completion assignment on Gradescope.
When you see Try it in ghci or Pause and predict, the activity is there to help you understand the material. There is nothing to submit for those boxes unless the box explicitly says Gradescope question.
Warmup: Lists and Function Types¶
Before introducing anything new, let's warm up with a few ideas from the previous modules.
A Haskell list has a type describing the type of its elements. For example:
[True, False, False]
is a list whose elements are all Bools.
Gradescope question: What is the type?
Submit this response on Gradescope.
What is the type of the expression:
[True, False, False]
Choose one:
Bool -> Bool -> BoolBool(Bool)[Bool][Bool, Bool, Bool]
The square brackets in a type mean "list of." So:
[Bool]
means "a list of Bools." The length of the list is not part of this type. A list with three Bools and a list with ten thousand Bools both have type [Bool].
Now recall from Module 02.2 that operators are functions too.
Gradescope question: Type of &&
Submit this response on Gradescope.
The type of the logical "and" operator, written && in Haskell, is:
(Bool, Bool, Bool)Bool -> Bool -> Bool(Bool, Bool) -> BoolBool[Bool, Bool, Bool]
You can always ask ghci directly:
> :type (&&)
(&&) :: Bool -> Bool -> Bool
Read this using currying from Module 02.2: && takes one Bool and returns a function that takes another Bool and returns a Bool. It is also convenient to say, more informally, that && takes two Bools and returns a Bool.
One more warmup:
Gradescope question: Which list is valid?
Submit this response on Gradescope.
Select the expression that does not cause an error in Haskell:
myList1 = [11, False, "howdy"]
myList2 = [11, 4.5, -107]
myList3 = [27, [1, 2], [10]]
myList4 = [[], [1, 2], [10]]
After you answer: what matters?
Haskell lists are homogeneous: every element of a list must have the same type.
The important question is not whether the values look similar. It is whether Haskell can assign one element type consistently to the entire list.
This rule is going to explain a lot of what happens in the next section.
Why Lists?¶
There are at least two good reasons for us to care about lists.
The practical reason: put related data together¶
Imagine storing three related values separately:
firstVal = 5
secondVal = 3
thirdVal = 19
Usually it is much more useful to collect them:
vals = [5, 3, 19]
Other languages give you analogous structures: arrays, linked lists, vectors, Python lists, and so on. The details differ, but the motivation is familiar.
Side by side, the contrast is the whole point:
first_val = 5 vals = [5, 3, 19]
second_val = 3
third_val = 19
Three names to declare, track, and pass around separately become one name that carries all three values together.
The pedagogical reason: lists are recursive¶
More importantly for this course, lists have a beautifully recursive structure.
A list is either:
- empty, or
- an element followed by another list.
That second clause defines a list in terms of another list. This is a recursive definition.
In Haskell notation, we can say:
A list is either
[], or it has the formx : xs.
Here:
[]is the empty list,xis one element, andxsis another list containing everything after that first element.
This is going to matter enormously because a recursive data structure naturally suggests recursive programs.
For example, a recursive function over a list often has exactly the same two cases:
- What should happen for
[]? - If the list has the form
x : xs, what should happen tox, and how should we recurse onxs?
That is the basic pattern we will use again and again.
Lists in Haskell¶
Basic list syntax¶
Haskell lists use square brackets, with elements separated by commas:
[4, 9, -13, 5, 1]
[False, True, False, False]
[3.1, 3.14, 3.141, 3.1419]
[]
[[1,2,3], [4,5,6,7], [8,9]]
The last example is a list whose elements are themselves lists.
Lists are homogeneous¶
Every element of a Haskell list must have the same type.
For example:
[True, False, True]
is fine because every element has type Bool.
But:
[15, True]
does not type-check. One element is numeric and the other is Boolean.
Haskell cannot pick one element type that fits both 15 and True, so [15, True] does not type-check.
Try it in ghci - nothing to submit
Try each of these:
[1, 2, 3]
[True, False]
["hello", "there"]
[[1,2], [3,4,5]]
[1, True]
For the last one, read the error message rather than immediately moving on. What two incompatible types does Haskell mention?
Strings are lists too¶
A Haskell String is a list of characters.
So:
"hello"
is another notation for:
['h', 'e', 'l', 'l', 'o']
and its type can be written either as:
String
or equivalently:
[Char]
This explains why ordinary list operations often work on strings.
Building Lists with :¶
The cons operator, written :, attaches one element to the front of a list.
3 : [7, 15, 4]
evaluates to:
[3, 7, 15, 4]
So : takes:
- one element on the left, and
- a list on the right.
That distinction is crucial.
Lists are built from : and []¶
Try:
3 : 7 : [15, 4]
Because : associates to the right, Haskell groups this as:
3 : (7 : [15, 4])
which gives:
[3, 7, 15, 4]
But this:
3 : 7 : 15 : 4
does not work. Eventually Haskell reaches:
15 : 4
and the right side of : is supposed to be a list, not a number.
We can fix it by ending with an empty list:
3 : 7 : 15 : 4 : []
Fully parenthesized:
3 : (7 : (15 : (4 : [])))
This is not merely another quirky syntax for lists. It exposes the recursive structure underneath ordinary bracket notation.
[3, 7, 15, 4]
is built from:
3 : (7 : (15 : (4 : [])))
The three steps in one place:
3 : [7, 15, 4] -- ok: an element consed onto a list => [3, 7, 15, 4]
3 : 7 : 15 : 4 -- error: the last : has 4 (a number) on its right
3 : 7 : 15 : 4 : [] -- ok: now every : has a list on its right => [3, 7, 15, 4]
What Types Can : Connect?¶
Suppose we write:
x : xs
If:
x :: Integer
then:
xs :: [Integer]
and the whole expression also has type:
[Integer]
But nothing about : is specific to integers.
We can also have:
Char and [Char]
Bool and [Bool]
Animal and [Animal]
[Integer] and [[Integer]]
or even:
(String, Shape) and [(String, Shape)]
The general pattern is:
left side: a
right side: [a]
result: [a]
where a can stand for any type.
Gradescope question: Types for the arguments of :
Submit this response on Gradescope.
List as many options as you can, four or more, for the types of the left and right arguments to the cons operator :.
Write each pair separated by a comma on a single line. For instance, you might write:
Bool, [Bool]
but see how creative you can get.
The important constraint
The left and right types are not independently arbitrary.
If the left side has type a, the right side must have type [a].
Ask ghci:
> :type (:)
(:) :: a -> [a] -> [a]
The lowercase a is a type variable. We will return to that idea near the end of the module.
Every row is the same shape: whatever type the left side has, the right side is a list of that type.
Concatenating Lists with ++¶
The operator ++ is different from :.
: attaches one element to the front of a list.
++ concatenates two lists:
[1, 3] ++ [7, 15, 4]
evaluates to:
[1, 3, 7, 15, 4]
The type is:
(++) :: [a] -> [a] -> [a]
Both inputs are lists, and their element types must match.
Because strings are lists of characters:
"Hello " ++ "world!"
also works.
A useful algebraic property¶
The empty list behaves as an identity for concatenation:
xs ++ [] == xs
[] ++ xs == xs
This is analogous to:
n + 0 = n
0 + n = n
Seeing these structural properties is useful. Functional data structures often have algebraic laws that can help us reason about what programs do.
Lists and Types: Practice¶
The difference between "an element" and "a list" takes some practice. The following Gradescope questions are designed to force that distinction.
Try to predict each answer before using ghci.
Gradescope question: What happens?
Submit this response on Gradescope.
What does this expression evaluate to? If it causes an error, explain why.
[1,2,3] : 5
After you answer: inspect the shape
The left side of : is itself a list, so Haskell could treat it as one element of a larger list. That part is fine.
The problem is the right side. : requires a list there, but 5 is not a list.
Gradescope question: What happens?
Submit this response on Gradescope.
What does this expression evaluate to? If it causes an error, explain why.
["hi"] : [["my"], ["name", "is"]]
After you answer: inspect the nesting
The left side has type [String].
The right side is a list whose elements also have type [String], so its type is [[String]].
That means the types line up correctly for cons:
[String] : [[String]]
Gradescope question: What happens?
Submit this response on Gradescope.
What does this expression evaluate to? If it causes an error, explain why.
9 ++ [12, 15, 18, 21, 24]
After you answer: remember what ++ expects
++ concatenates two lists.
The left operand 9 is a single number, not a list. Compare it with:
[9] ++ [12, 15, 18, 21, 24]
Gradescope question: What happens?
Submit this response on Gradescope.
What does this expression evaluate to? If it causes an error, explain why.
(4 : []) ++ (3 : 2 : 1 : [])
After you answer: translate each side
First simplify each list:
4 : [] == [4]
3 : 2 : 1 : [] == [3,2,1]
Then concatenate:
[4] ++ [3,2,1]
Try a few more in ghci - nothing to submit
Predict first, then test:
[] : []
[True] ++ []
'H' : "ello"
[1] : [[2,3], [4,5]]
For each expression, ask two questions:
- What is the type of the left operand?
- Does the right operand have the shape required by the operator?
Useful Functions on Lists¶
Haskell has many built-in functions for working with lists. We will start with a small toolbox.
length¶
length counts the elements in a list:
length ["Hello", " world", "!"] -- 3
length "Hello" -- 5
length [] -- 0
Its type is:
length :: [a] -> Int
There is a nice structural fact here:
length [] == 0
and:
length (xs ++ ys) == length xs + length ys
Concatenating the lists corresponds to adding their lengths. This is an example of an algebraic law that can help us reason about code.
head and tail¶
head returns the first element:
head ["Hello", ",", "world", "!"]
-- "Hello"
tail returns everything after the first element:
tail ["Hello", ",", "world", "!"]
-- [",", "world", "!"]
Think again about the recursive form:
x : xs
Then:
head (x : xs) == x
tail (x : xs) == xs
This explains a possibly surprising example:
tail [3]
gives:
[]
because:
[3] == 3 : []
and the tail is exactly the [].
head [] and tail []
What is the first element of an empty list? There isn't one.
What is everything after the first element of an empty list? Again, there is no first element.
So:
head []
tail []
both fail at runtime.
This is one reason pattern matching will soon be preferable to blindly calling head and tail: patterns can force us to think explicitly about the empty-list case.
take and drop¶
take keeps the first n elements:
take 3 [5, 8, 9, 1, 13, 20, 3, 2]
-- [5,8,9]
drop removes the first n elements:
drop 2 [5, 8, 9, 1, 13, 20, 3, 2]
-- [9,1,13,20,3,2]
Their types are:
take :: Int -> [a] -> [a]
drop :: Int -> [a] -> [a]
null¶
null asks whether a list is empty:
null [] -- True
null [3,-7] -- False
null "" -- True
null "hello" -- False
Again, the string examples work because strings are lists of characters.
elem¶
elem asks whether a value occurs in a list:
elem 10 [30, 33, 37] -- False
elem '3' "CS131" -- True
10 `elem` [5, 10, 15, 20] -- True
Because elem is a two-argument function, we can also write it infix using backticks.
Writing a Recursive Function on a List¶
Suppose Haskell did not already have elem.
How could we write a function that determines whether a value occurs in a list?
Let's call it:
inList e list
The recursive structure of the list gives us the algorithm.
Step 1: Find the base case¶
If the list is empty:
[]
then e cannot be in it.
So:
if the list is empty
return False
Step 2: Inspect the first element¶
If the list is nonempty and its first element is e, then we are done:
if the first element is e
return True
Step 3: Recurse on the rest¶
Otherwise, the answer depends on the remainder of the list:
check whether e is in the rest of the list
Using the functions we already know:
inList e list =
if null list
then False
else if head list == e
then True
else inList e (tail list)
The interesting part here is not that we reinvented elem. The interesting part is the shape of the reasoning:
- empty list gives the base case,
- nonempty list lets us inspect one element,
- recursive call handles the rest.
That structure will become much cleaner once we introduce pattern matching.
Try it in a file - nothing to submit
Put inList into a .hs file and try:
inList 3 [1,2,3,4]
inList 9 [1,2,3,4]
inList 'a' "Haskell"
You have now implemented a simplified version of something built into the language.
From Repetition to Abstraction: map¶
Now suppose we want to add 3 to every element of a list.
We might write:
add3List :: [Integer] -> [Integer]
add3List list =
if null list
then list
else (+ 3) (head list) : add3List (tail list)
Then perhaps we want to double every element:
doubleList :: [Integer] -> [Integer]
doubleList list =
if null list
then list
else (* 2) (head list) : doubleList (tail list)
And then add an exclamation point to every string:
emphasizeList :: [String] -> [String]
emphasizeList list =
if null list
then list
else (++ "!") (head list) : emphasizeList (tail list)
These functions look suspiciously similar.
Pause and identify the pattern - nothing to submit
Compare the three definitions.
What varies?
What stays exactly the same?
The only meaningful difference is the function applied to each element:
(+ 3)
(* 2)
(++ "!")
Everything else is the same recursion scaffold.
This gives us a recurring computer science lesson:
Where there is a pattern, there is an opportunity for abstraction.
Instead of hard-coding the operation, make the operation itself a parameter:
applyFuncToList f list =
if null list
then list
else f (head list) : applyFuncToList f (tail list)
But Haskell already provides this abstraction. It is called map:
map :: (a -> b) -> [a] -> [b]
For example:
doubler x = 2 * x
map doubler [5, 7, 13]
-- [10,14,26]
Now remember Module 02.2: functions are values, and we can partially apply operators.
So we do not need to give doubler a name at all.
Gradescope question: Make the map expression shorter
Submit this response on Gradescope.
In the module we saw code that looked something like this:
doubler x = 2 * x
map doubler [5, 7, 13]
How can you write a single line of code that evaluates to the same thing?
After you answer: use a function value directly
One possibility is:
map (* 2) [5, 7, 13]
(* 2) is itself a function. map receives that function as its first argument.
This is exactly the kind of programming that becomes possible once functions are ordinary values.
map can change the element type¶
Notice the type again:
map :: (a -> b) -> [a] -> [b]
The input list contains as.
The output list contains bs.
They do not need to be the same type.
For example:
map length ["Haskell", "Python", "Java"]
-- [7,6,4]
The input is:
[String]
and the output is:
[Int]
because:
length :: [a] -> Int
In this particular use, each input element is a String, and each result is an Int.
Try it in ghci - nothing to submit
Predict the type and result of each:
map (* 10) [1,2,3]
map even [1,2,3,4]
map length ["Haskell", "Python", "Java"]
map show [10,20,30]
filter: Keep the Elements That Pass a Test¶
Another extremely common list pattern is to keep only elements satisfying some condition.
Haskell's filter function has type:
filter :: (a -> Bool) -> [a] -> [a]
The first argument is a predicate, meaning a function that returns Bool.
Examples:
filter even [8, -13, 25, 16, 0, 2]
-- [8,16,0,2]
filter (< 3) [8, -13, 25, 16, 0, 2]
-- [-13,0,2]
filter ((< 3) . length) ["oh", "hi", "there!"]
-- ["oh","hi"]
Notice an important difference between map and filter.
map transforms each element and preserves the number of elements.
filter preserves the elements themselves but may remove some of them.
There are many other useful list functions, including:
minimum
maximum
sum
product
When you are working with lists, it is worth asking whether Haskell already provides the operation you need. Unless an assignment specifically asks you to implement something yourself, using existing list operations is encouraged.
Pattern Matching: Use the Shape of the Data¶
So far we have built lists using :.
Now we get one of Haskell's most important ideas:
When
:appears in a pattern, it can take a list apart.
Consider:
firstInt : otherInts = [13, 10, 20, 7]
The pattern on the left has the shape:
firstInt : otherInts
The value on the right can be thought of as:
13 : 10 : 20 : 7 : []
Haskell matches the pieces:
firstInt = 13
otherInts = [10,20,7]
The syntax used to construct a list is therefore also useful for deconstructing it.
Haskell lines the pattern up against the value one position at a time: first_int binds the head, other_ints binds the rest.
Matching several elements¶
Because : associates to the right, a pattern can expose more than one element:
c1 : c2 : rest = "aloha"
Remember:
"aloha" == ['a','l','o','h','a']
So the pattern is really matching:
c1 : (c2 : rest)
against:
'a' : ('l' : ['o','h','a'])
Gradescope question: Pattern matching a string
Submit this response on Gradescope.
What is the value of s after evaluating:
c1 : c2 : s = "aloha"
After you answer: peel the list apart
The first two characters are bound to c1 and c2.
Everything remaining is bound to s.
Since a string is a list of characters, s is itself a string.
case Expressions¶
Pattern matching is useful whenever our computation depends on the shape of a value.
A case expression lets us provide several patterns:
case b of
True -> "yep!"
False -> "nope!"
If b is True, the first branch is chosen. If it is False, the second branch is chosen.
We can match literal values too:
case n of
5 -> "I love 5!"
10 -> "10 is okay"
_ -> "I do not know that number"
The underscore _ is a wildcard pattern. It matches anything, but does not bind a name.
Patterns must make sense for the value's type¶
This is invalid:
case 14 of
True -> "yep!"
False -> "nope!"
The value being matched is numeric, but the patterns True and False have type Bool.
The value 14 is numeric, but the patterns True and False demand that it be a Bool.
Patterns should cover the possible inputs¶
This expression is also dangerous:
case n of
5 -> "I love 5!"
10 -> "10 is okay"
If n is 100, neither pattern matches. A non-exhaustive match can fail at runtime.
Adding:
_ -> "I do not know that number"
provides a catch-all case.
Without a catch-all branch, a value that matches none of the patterns is a runtime crash, not a compile-time error.
Pattern Matching on Lists¶
Lists have exactly the recursive structure we need for pattern matching.
A list is either:
[]
or:
x : xs
That means many recursive list functions can be written with those two patterns.
myLength with a case¶
Earlier we could write:
myLength lst =
if lst == []
then 0
else 1 + myLength (tail lst)
Using a case:
myLength lst =
case lst of
[] -> 0
(_:xs) -> 1 + myLength xs
The patterns describe exactly the two shapes a list can have.
Notice the wildcard:
_:xs
We do not care what the first element is. We only need to know that there is one, and then recursively compute the length of the rest.
Pattern matching directly in a function definition¶
Haskell lets us go one step further:
myLength [] = 0
myLength (_:xs) = 1 + myLength xs
This is the same structure, but the pattern matching has moved directly into the function definition.
Read it almost like a mathematical definition:
The length of the empty list is
0.The length of a nonempty list is
1plus the length of its tail.
This is one reason recursion can feel unusually natural in Haskell: the program follows the structure of the data.
Another example: myFilter¶
Using case:
myFilter test lst =
case lst of
[] -> []
(x:xs) ->
if test x
then x : myFilter test xs
else myFilter test xs
Using pattern matching in the function definition:
myFilter _ [] = []
myFilter test (x:xs) =
if test x
then x : myFilter test xs
else myFilter test xs
The empty-list case gives the base case. The nonempty-list case gives us exactly the two pieces we need: the first element x and the remaining list xs.
A pattern to remember
When writing a recursive function on a list, try starting with:
f [] = ...
f (x:xs) = ...
Then ask:
- What is the right answer for the empty list?
- What should I do with
x? - How can I use the recursive result on
xs?
That template will carry you surprisingly far.
Quicksort: Putting the Pieces Together¶
At this point, we have several ideas that are each useful on their own:
- lists give us a recursive data structure,
- pattern matching lets us split a list into its empty and nonempty cases,
- recursion lets us solve the same problem on smaller lists,
- higher-order functions such as
filterlet us describe common list-processing patterns without writing the traversal ourselves.
A nice example that combines all four ideas is quicksort.
Here is a compact version that sorts a list of integers:
quicksort_ints :: [Integer] -> [Integer]
quicksort_ints [] = []
quicksort_ints (x:xs) =
quicksort_ints (filter (<= x) xs)
++ [x] ++
quicksort_ints (filter (> x) xs)
There is a lot packed into these few lines.
The first pattern:
quicksort_ints [] = []
handles the base case. An empty list is already sorted.
The second pattern:
quicksort_ints (x:xs) = ...
handles a nonempty list. Pattern matching gives us:
x, the first element, which we will use as a pivot, andxs, the rest of the list.
We then use the higher-order function filter twice:
filter (<= x) xs
collects the elements less than or equal to the pivot, while:
filter (> x) xs
collects the elements greater than the pivot.
Each of those lists is smaller than the original list, so we recursively sort both of them:
quicksort_ints (filter (<= x) xs)
quicksort_ints (filter (> x) xs)
Finally, we concatenate the three pieces:
sorted-smaller ++ [x] ++ sorted-larger
For example:
quicksort_ints [5, 2, 8, 1, 4]
conceptually becomes:
quicksort_ints [2, 1, 4]
++ [5] ++
quicksort_ints [8]
and recursion continues until all of the lists reach the empty-list base case.
What is striking here is how closely the code follows the idea of the algorithm. We do not need array indices, mutable variables, for loops, or an explicit accumulator. The structure comes almost entirely from the tools we have been developing:
Pattern-match on the structure of the data, use higher-order functions to describe what should happen to its pieces, and recurse on the smaller pieces.
This combination of lists + pattern matching + recursion + higher-order functions is one of the places where functional programming can become both very concise and very expressive.
Lists Are Lazy Too¶
Module 02.2 introduced lazy evaluation. Lists give us one of the most striking uses of it.
Haskell can represent:
[1..]
an infinite list of integers.
Obviously Haskell cannot finish constructing every integer before doing anything else.
But under lazy evaluation, it does not need to.
We can ask:
take 5 [1..]
and get:
[1,2,3,4,5]
Only the portion demanded by take 5 needs to be produced.
This is a nice concrete example of why lazy evaluation changes what kinds of data structures are convenient to express.
Try it in ghci - nothing to submit
Try:
take 10 [1..]
take 8 [2,4..]
take 5 (map (* 10) [1..])
Do not ask ghci to print [1..] by itself unless you are prepared to interrupt it.
Tuples: Grouping Different Types¶
Lists are useful when we want any number of elements of the same type.
Sometimes we want a fixed collection whose pieces have different types.
For that, Haskell has tuples.
For example:
prof1 = ("Lucas Bang", 131, False)
prof2 = ("Ben Wiedermann", 131, True)
Each tuple contains:
- a
String, - a number representing a course, and
- a
Bool.
Unlike lists, tuples:
- may contain different types,
- have a fixed number of positions, and
- include their number and order of component types in the tuple type.
For example, a value like:
("Lucas Bang", 131, False)
might have a type such as:
(String, Integer, Bool)
A pair such as:
("hello", True)
has a different tuple type:
(String, Bool)
Tuples with different numbers of components are different kinds of types.
Pattern Matching on Tuples¶
Pattern matching works beautifully on tuples too.
Suppose:
prof1 = ("Lucas Bang", 131, False)
Then:
(name, course, acted) = prof1
matches each position and binds:
name = "Lucas Bang"
course = 131
acted = False
The pattern has the same structure as the value.
We can use tuple patterns directly in function definitions:
isRightTriangle :: (Integer, Integer, Integer) -> Bool
isRightTriangle (a, b, c) = a * a + b * b == c * c
Then:
isRightTriangle (5,12,13) -- True
isRightTriangle (3,4,5) -- True
isRightTriangle (3,4,6) -- False
If we only care about one position, wildcards help:
getCourse (_, course, _) = course
Then:
getCourse prof2
-- 131
A tuple pattern has the same shape as the tuple it matches: each name binds the value in its position.
Try it in ghci - nothing to submit
Try:
let (x,y) = (10,20) in x + y
Then define:
firstOfPair (x, _) = x
secondOfPair (_, y) = y
What types does ghci infer?
Parameterized Types¶
We have been quietly using type variables throughout the module.
Now let's make the idea explicit.
Ask ghci for these types:
> :type (:)
(:) :: a -> [a] -> [a]
> :type (++)
(++) :: [a] -> [a] -> [a]
> :type map
map :: (a -> b) -> [a] -> [b]
> :type filter
filter :: (a -> Bool) -> [a] -> [a]
The lowercase names a and b are type variables.
They stand for types that are not fixed in advance.
Same letter means same type¶
Consider:
(:) :: a -> [a] -> [a]
If a is Bool, the type becomes:
Bool -> [Bool] -> [Bool]
If a is Char:
Char -> [Char] -> [Char]
If a is [Integer]:
[Integer] -> [[Integer]] -> [[Integer]]
The key rule is:
Every occurrence of the same type variable within one use must be instantiated consistently.
That is why:
True : [False, True]
works, while trying to cons a Bool onto a list of Chars does not.
Different letters may be different types¶
Now consider:
map :: (a -> b) -> [a] -> [b]
There are two type variables, a and b.
They may happen to be the same:
map (* 2) [1,2,3]
takes numbers to numbers.
But they need not be:
map show [1,2,3]
takes numbers to strings.
Or:
map even [1,2,3]
takes numbers to Bools.
This is why map is so general.
A list type is parameterized¶
Think about:
[a]
It describes a list whose element type is not yet specified.
We can instantiate that parameter in many ways:
[Integer]
[Bool]
[Char]
[String]
[(String, Bool)]
[[Integer]]
So [a] describes a whole family of list types, parameterized by the element type.
This notion of parameterized types is going to become even more important when we start defining our own data types.
All those concrete rows collapse into one signature — (:) :: a -> [a] -> [a] — with both as required to agree.
Reading the Types of head and tail¶
Let's finish by deriving two types rather than memorizing them.
What must head do?¶
head consumes a list:
head [10,20,30]
and returns one element:
10
If the input has type:
[a]
then one element has type:
a
So:
head :: [a] -> a
What must tail do?¶
tail also consumes a list:
tail [10,20,30]
but returns another list:
[20,30]
If the input has type [a], the remainder still has type [a].
So:
tail :: [a] -> [a]
Gradescope question: Type of tail
Submit this response on Gradescope.
What is the type of tail?
Choose one:
[a] -> [a][a -> [a][a] -> aa -> a
Gradescope question: Type of head
Submit this response on Gradescope.
What is the type of head?
Choose one:
a -> aa -> [a]a -> [b][a] -> a
Do not memorize these in isolation
The types tell the story:
head :: [a] -> a
tail :: [a] -> [a]
head removes one level of "list-ness."
tail preserves it.
This way of deriving a type from what a function must do is much more useful than memorizing a table of signatures.
How the Pieces Fit Together¶
This module introduced several ideas, but they reinforce one another.
Lists expose recursive structure¶
A list is:
[]
or:
x : xs
That gives recursive functions a natural base case and recursive case.
Pattern matching lets programs follow that structure directly¶
Instead of repeatedly asking:
null list
head list
tail list
we can say:
f [] = ...
f (x:xs) = ...
The structure of the program mirrors the structure of the data.
Higher-order functions abstract recurring recursion patterns¶
Functions such as map and filter capture common ways of recursively traversing lists.
Instead of rewriting the traversal every time, we supply the part that varies: another function.
Parameterized types describe the abstraction¶
Types such as:
map :: (a -> b) -> [a] -> [b]
tell us that the same list-processing structure works for many different element types.
Tuples give us a complementary kind of structure¶
Lists are homogeneous and variable-length.
Tuples are fixed-length and can combine different types.
Pattern matching works on both.
These ideas give us a much richer vocabulary for organizing programs than "variables plus loops." We can think about the shape of data, write functions that mirror that shape, and then abstract repeated computation patterns into reusable higher-order functions.
Where This Leaves Us¶
You should now be comfortable with:
[]
(:)
(++)
length
head
tail
take
drop
null
elem
map
filter
More importantly, you should understand the structural ideas underneath them:
- a list is either empty or
x : xs, - recursive functions can mirror that structure,
- pattern matching deconstructs values according to their shape,
- functions such as
mapabstract recurring list traversals, - tuples let us group heterogeneous values,
- type variables describe general relationships between types.
Module 03.2 will take the next step: instead of using only data structures Haskell already gives us, we will define new data types of our own.
Finish the Module 3.1 Completion¶
At this point, you should have encountered every substantive question in the Module 3.1 Completion assignment on Gradescope.
Before submitting, Gradescope also asks you for two pieces of feedback that are not content questions:
Gradescope: Time spent
Submit this response on Gradescope.
Approximately how much time did you spend on this module?
Gradescope: Remaining questions and thoughts
Submit this response on Gradescope.
What lingering questions or thoughts do you have about this module?
That is the end of Module 03.1.