Writing list functions

We’ve now seen a number of operations on lists:

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:

  1. the empty list, called empty
  2. 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:

1
empty and [list: ] are the same list!