Writing list functions
We’ve now seen a number of operations on lists:
member
distinct
append
length
filter
all
Today we’ll learn how to write our own list operations.
A sum
function
How would we write a function that takes a list of functions and returns its sum? We can start out by writing the skeleton of such a function:
fun my-sum(lst :: List<Number>) -> Number: ... end
We’re calling the function my-sum
because Pyret includes a sum
function. Can
we use any of the list functions we already know about in order to write such a
function? For instance, could we use filter
?
filter
can’t be right–it takes a list, which is what we want, but it returns
a boolean. length
takes a list and returns a number, but we know it’s not the
number we want. map
takes a list and returns a list. It seems like none of
these functions is going to work!
OK, so we don’t yet know how to write this function. Let’s procrastinate on writing the function body by writing out some examples!
fun my-sum(lst :: List<Number>) -> Number: ... where: my-sum([list: 3, 1, 4]) is 3 + 1 + 4 my-sum([list: 1, 4]) is 1 + 4 my-sum([list: 4]) is 4 my-sum([list: ]) is ... end
What’s the sum of an empty list of numbers? It’s zero.
These examples should start to suggest a pattern (they were chosen for that reason)! We can rewrite them to illustrate this pattern:
fun my-sum(lst :: List<Number>) -> Number: ... where: my-sum([list: 3, 1, 4]) is 3 + my-sum([list: 1, 4]) my-sum([list: 1, 4]) is 1 + my-sum([list: 4]) my-sum([list: 4]) is 4 + my-sum([list: ]) my-sum([list: ]) is 0 end
In the top three cases, we’re taking the first element of the list and adding it
to my-sum
called on the rest of the list. In the last, empty case, we’re doing
something a little different–when the list is empty we can’t take the first
element, so we’re just going to return 0.
How can we use this in order to complete our my-sum
function? In order to do
so, we’ll need to learn a bit about the SECRET NATURE OF LISTS.
The secret nature of lists
The notation we’ve been using to write lists (for example, [list: 3, 1, 4]
) is
actually shorthand for what’s actually going on behind the scenes. Pyret knows
about two ways of building a list:
- the empty list, called
empty
- An element added onto the front of a list, called
link(fst :: A, rst :: List)
When we write a list using the [list: ...]
notation, Pyret translates it into
an expression using link
and empty
. So for instance, [list: 3, 1, 4]
is
translated into
link(3, link(1, link(4, empty)))
How can we use this to complete our my-sum
function?
The cases
expression
Now that we know the secret nature of lists, we can ask Pyret how a given list
was built. We do this using the cases
expression:
fun my-sum(lst :: List<Number>) -> Number: cases (List) lst: | empty => ... | link(fst, rst) => ... end end
What’s going on here? We can think of cases
as being sort of like an if
: if
the list is empty
we should do one thing; if the list is a link
, we should
do another thing. cases
also lets us define names for the components of a
link
: in this case we’ve named them fst
and rst
, but we could have called
them head
and tail
, bert
and ernie
, or anything else (please don’t call
them bert
and ernie
). We can use fst
and rst
to compute the answer in
the link
case.
How should we fill out these cases? Our answer comes from the where
-block
examples we’ve written. Since my-sum([list: ]) is 0
, we know that we should
return zero in the empty
case:1
fun my-sum(lst :: List<Number>) -> Number: cases (List) lst: | empty => 0 | link(fst, rst) => ... end end
The link
case comes from our examples two! In each case, we took the first
element of the list (fst
) and added it to the sum of the rest of the list
(my-sum(rst)
). So we should have:
fun my-sum(lst :: List<Number>) -> Number: cases (List) lst: | empty => 0 | link(fst, rst) => fst + my-sum(rst) end where: my-sum([list: 3, 1, 4]) is 3 + my-sum([list: 1, 4]) my-sum([list: 1, 4]) is 1 + my-sum([list: 4]) my-sum([list: 4]) is 4 + my-sum([list: ]) my-sum([list: ]) is 0 end
Let’s look at how this function evaluates:
my-sum(link(3, link(1, link(4, empty)))) 3 + my-sum(link(1, link(4, empty))) 3 + 1 + my-sum(link(4, empty)) 3 + 1 + 4 + my-sum(empty) 3 + 1 + 4 + 0
The all-below-10
function
Let’s work on another function over lists of numbers. all-below-10
should
return true if every member of the list is less than 10, and false
otherwise. We’ve seen one way of writing it:
fun all-below-10(lst :: List<Number>) -> Boolean: all(lam(x): x < 10 end, lst) end
This will work fine, but let’s try to write it with cases
. Once we’ve done
that, we’ll be able to see how all
is actually implemented!
As we did with my-sum
, we’ll start with some tests:
fun all-below-10(lst :: List<Number>) -> Boolean: ... where: all-below-10([list: 3, 1, 4]) is (3 < 10) and (1 < 10) and (4 < 10) all-below-10([list: 1, 4]) is (1 < 10) and (4 < 10) all-below-10([list: 4]) is (4 < 10) all-below-10([list: ]) is ... end
What should go in that last case? Are all of the numbers in an empty list below
10? Well, are there any that aren’t? Since there are not, let’s say it’s true
.
We can rewrite these tests again:
fun all-below-10(lst :: List<Number>) -> Boolean: ... where: all-below-10([list: 3, 1, 4]) is (3 < 10) and all-below-10([list: 1, 4]) all-below-10([list: 1, 4]) is (1 < 10) and all-below-10([list: 4]) all-below-10([list: 4]) is (4 < 10) and all-below-10([list: ]) all-below-10([list: ]) is true end
This suggests what our function body should look like:
fun all-below-10(lst :: List<Number>) -> Boolean: cases (List) lst: | empty => true | link(fst, rst) => (fst < 10) and all-below-10(rst) end where: all-below-10([list: 3, 1, 4]) is (3 < 10) and all-below-10([list: 1, 4]) all-below-10([list: 1, 4]) is (1 < 10) and all-below-10([list: 4]) all-below-10([list: 4]) is (4 < 10) and all-below-10([list: ]) all-below-10([list: ]) is true end
Now that we’ve seen how this works, we can see how all
is implemented: it’s
exactly the same, except that it applies some particular function instead of
just checking if numbers are less than 10:
fun my-all(f, lst :: List) -> Boolean: cases (List) lst: | empty => true | link(fst, rst) => f(fst) and my-all(f, rst) end end
Footnotes:
empty
and [list: ]
are the same list!