Matching and Guards and Scope of Variables
In the previous section we wrote a little example program for converting temperatures. In creating programs like that with special data structures (in our case, a list of cities and their temperatures), it's often useful to create utility functions which make working with our data more convenient. We will explore that below and use these functions to introduce some new concepts.
A Utility Function
In this case, it could be useful to find the maximum and minimum temperature in our data. We can add support for this by creating the necessary code little bit at a time. Let's start with creating functions for finding the maximum value of the elements of a property list:
(defmodule tut9
(export (list-max 1)))
(defun list-max
(((cons head tail))
(list-max tail head)))
(defun list-max
(('() results)
results)
(((cons head tail) result-so-far) (when (> head result-so-far))
(list-max tail head))
(((cons head tail) result-so-far)
(list-max tail result-so-far)))
Then, in the LFE REPL:
> (c "tut9.lfe")
#(module tut9)
> (tut9:list-max '(1 2 3 4 5 6 7 4 3 2 1))
7
Pattern Matching
Before we talk about pattern matching, let's clarify why we have two different functions with the same name. Note that the two list-max
functions above each take a different number of arguments (parameters). Or, another way of saying it: they have different arity. In LFE, functions having the same name but differing in arity are actually different functions. Where we need to distinguish between these functions we write <function name>/<arity>
, where <arity>
is an integer representing the number of arguments that function takes. For our example above, we would write list-max/1
and list-max/2
.
The next thing we should explain is the arguments for the list-max/2
function, since that probably looks pretty strange the first time you see it. If you look closely, you will see that there are three clauses in list-max/2
and each clause stats with the function arguments for that clause; in order, they are:
- an empty list and
results
- a
cons
andresults-so-far
with something called a guard (more on that soon) - a
cons
andresults-so-far
just by itself
What each of these are doing is what is called pattern matching in LFE: if the arguments passed to list-max/2
match the first pattern, the first clause gets executed and the others don't. If the first one does not match, the second one is tried, and so on.
So what is being "matched" here? Well, the first clause will match if it's first argument is an empty list. The second and third will match if the first element is a list. The second has something more, though: let's take a closer look.
Guards
In the second clause of list-max/2
we see a new form: (when ...)
which contains a comparison operation. The special form when
is a something we can use with LFE patterns to limit a match. In this case we use it to say: "only use this function clause if head
is greater than result-so-far
. We call tests of this type a guard. If the guard isn't true (usually referred to as "if the guard fails"), then we try the next part of the function.
Stepping Through the Function
Now that we know how there can be two functions with the same name and how the arguments for list-max/2
are working, let's step through the functions above. They represent an example of walking through a list and "carrying" a value as we do so, in this case result-so-far
is the variable that carries a value as we walk. list-max/1
simply assumes that the max value of the list is the head of the list and calls list-max/2
with the rest of the list and the value of the head of the list, in the above this would be (list-max '(2 3 4 5 6 7 4 3 2 1) 1)
. If we tried to use list-max/1
with an empty list or tried to use it with something which isn't a list at all, we would cause an error. The LFE philosophy is not to handle errors of this type in the function they occur, but to do so elsewhere. More about this later.
In list-max/2
we walk down the list and use head
instead of result-so-far
when head
is greater than result-so-far
. In this function, if head
isn't greater than result-so-far
then it must be smaller or equal to it, and the next clause is executed under this condition.
To change the above program to one which works out the minimum value of the element in a list, all we would need to do is to write <
instead of >
in the guard ... but it would be wise to change the name of the function to list-min
:-).
Scope and let
In a function, the arguments that are passed to it are in scope or "accessible" for all of that function. In another function which has been passed its own arguments, only those are in scope; arguments from other functions are not available for use.
In the case of functions which are pattern matching on the arguments, like our list-max/2
, we have three clauses, each with their own arguments and each with their own scope. The parentheses at the beginning of each clause mark this scope. In that case, the results
variable is only available in the first clause; it is not in scope for the second two clauses.
But passing arguments to functions and pattern matching function arguments are not the only ways in which you can bind a value to a variable. There is a special form in LFE (and other Lisps) called let
. Let's1 take a look, shall we? Here's another way we could have written list-max/2
:
(defun list-max
(('() results)
results)
(((cons head tail) result-so-far) (when (> head result-so-far))
(let ((new-result-so-far head))
(list-max tail new-result-so-far)))
(((cons head tail) result-so-far)
(list-max tail result-so-far)))
We only made a change to the second clause: we assigned the value of head
to the variable new-result-so-far
. This assignment didn't involve any computation or new values, it was essentially just a "renaming" mostly for the sake of demonstration, and arguably to make it more clear the purpose of the value stored in head
.
1 We are not going to apologize for that pun.