2 Programming in Pyret
Programs exist to compute answers, which we will call
values. These values will represent all sorts of interesting
phenomena: they may be a frame from the next hit movie, a list of Web
pages matching your query, a confirmation code for an upcoming trip,
or a prediction of tomorrow’s temperature. Ultimately, they’re all
values and our goal here will be to learn how to write programs that
compute them.
2.1 Values
The simplest programs are ones whose value we already
know.
What use is it if we already know it? You’ll find
out later: Examples and Testing: Predicting Behavior. Here, then, is the simplest
program you can write:
If you ask Pyret for the value of this program, it will unsurprisingly
tell you that it’s 3. Programs can also refer to text, called a
string:
The value of this program, likewise, is "Hello, Tara!".
Okay, so we’ve seen numbers and strings. There are many more kinds of
values in Pyret, including images. We’ll get to them soon.
2.2 Expressions
Obviously, we’re not very interested in writing programs whose answers
we already know. Therefore, let’s start the first real step of our
journey: writing expressions that take one or more steps to
produce an answer.
Do Now!
What is this program’s value?Yes, you must put spaces
around +.
As you might have guessed, this program’s value is 3. In your
head, you probably applied the rules of algebra to compute this
program’s value; in fact, Pyret follows the same
rules. Everything you know about how to compute the answer of an
algebraic expression applies here! Similarly, once I tell you that
using + on string concatenates them, you can easily see that
evaluates to "will.i.am".
2.2.1 Aggregate Values
You can, of course, make larger values: ones that contain other
values. For instance, you might want to represent a shopping list; in
Pyret, you can represent this as a list.
A list is actually a recursive data structure, whose base case is the
empty list, empty, and whose non-empty values are constructed
using link. For instance, here is a shopping list:
shopping = link("bread", link("water", empty))
We can take it apart using .first and .rest:
will evaluate to "bread",
to link("water", empty),
to "water", and so on.
Understanding the above recursive structure is crucial to being able
to structure programs that process lists. However, it can get tedious
to write all these link and empty elements every
time. Therefore, Pyret offers a convenient alternative notation for
constructing a list:
This creates a list that is exactly the same as
shopping.There is nothing sacred about
list:: Pyret offers a general mechanism for creating such
constructors, so if you make up a new kind of compound datum of
your own, with a little extra work it too can be constructed using
this same notation [REF].
Sometimes, you might not care about the order of elements; in that
case, you can use a set instead:
Later (
Sets Appeal) we will learn a great deal more about set
representations and implementations.
2.2.2 Precedence
If you try to write expressions that combine operators, such as
Pyret wants you to be clearer about your intent: did you mean for this
to evaluate as
(i.e., 0), or
(i.e., 2), or ...? Instead, Pyret simply
asks that you parenthesize expressions that combine operators, so
either of the interpretations given above is a valid Pyret program.
2.3 Functions: Abstracting Over Expressions
Suppose we want to know how much we would weigh when we get to the
moon. As you might know, our moon weight is one-sixth that on earth
(suggesting an especially expensive but effective dieting
strategy). Thus, the moon weight of a person who weighs 100 pounds or
kilograms would be 100 * (1 / 6). If instead a person weighed
150 (the units don’t really matter), their moon weight would be
150 * (1 / 6). Suppose they weighed 200 on earth? Then on the
moon, it would be 200 * (1 / 6). And so on.
Already, we see that it is annoying to have to keep writing 1 /
6. It would be much better to give that expression a meaningful name
and use its name everywhere. This would:
improve the program’s readability, by giving this mysterious
number a name that explains it;
reduce error, because we if we type it each time, we might
accidentally mis-type it one time (and rely on this incorrect
answer); and,
easily update our knowledge: if we find the answer needs to
change (for instance, we might improve its precision), we would need
to make that change in only one place.
This is such a common and important concept in writing software that
it has a name: the DRY Principle, where DRY stands for
“Don’t Repeat Yourself”.
Therefore, we can rewrite the preceding sequence of expressions as:
earth-to-moon = 1/6
100 * earth-to-moon
150 * earth-to-moon
200 * earth-to-moon
This is much better, but notice we’ve again violated the DRY
principle: the entire expression something * earth-to-moon, for
different values of something, has been repeated.
To avoid this kind of repetition, we apply a similar idea: give a name
to the repeated expression. However, what is in common needs to have a
“hole” to fill in the part that differs. This hole is called a
argument or parameter, and the resulting named
expression—with one or more holes in it—is a
function. Here’s the function that captures the above pattern:
fun moon-weight(earth-weight):
earth-weight * earth-to-moon
end
We can use it to compute the preceding three values as follows:
moon-weight(100)
moon-weight(150)
moon-weight(200)
Notice that the arguments for the DRY principle apply again: we’ve
improved readability, reduced error, and given ourselves a single
place at which to improve the program.
2.4 Examples and Testing: Predicting Behavior
So, we’ve written a program. But how can we trust its output? Usually
our programs will be much larger than these; they might run for hours
and days; and at the end, they spit out a value. How can we have any
confidence in this value before we go off to make decisions that rely
on its correctness? This is our first brush with
predictability.“Be careful about reading health
books. You may die of a misprint.”—attributed to Mark Twain
There is no magic recipe for this, but there is a great deal we can do
to both gain confidence in our programs and learn when we should lose
confidence in one. The simplest thing you can do is write down, in
the program itself, your prediction of what it should produce: that
is, write down its expected answer. If you do this for a variety of
smaller inputs, and you’ve thought hard about covering the different
kinds of small inputs, then you can have significant confidence that
the program works on large inputs as well. This is called
testing. We will teach you much more about this and other
methods as we progress through this book. A special case of tests,
which you write based just on the problem statement—independent of
and even before determining the program’s behavior—we call
examples.
First, let’s learn Pyret’s mechanics for writing tests. You tell Pyret
to check your claims:The parentheses are not
strictly necessary, but help make clear what’s being tested.
check:
(1 + 2) is 3
("will." + "i." + "am") is "will.i.am"
end
Every time you run a program with this code, Pyret will check to
ensure these claims are still true.
Do Now!
If instead you claim:
check:
("will." + "i." + "am") is "william"
end
What will happen?
Pyret will report an error, informing you that "will.i.am" (the
value produced by the expression) did not match "william" (the
value you told it to expect).
Let’s go back to our lunar weight example. Now that we’ve found a
better way to write the computation (using a function), is there any
use left for the original expressions? There sure is! We now have two
different ways of computing what is supposedly the same answer. This
kind of (hopefully) redundant statement of program
behavior—saying something two different ways with the expectation
that the two ways will produce the same outcome—is the heart of
testing! For instance:
check:
moon-weight(100) is 100 * earth-to-moon
moon-weight(150) is 150 * earth-to-moon
moon-weight(200) is 200 * earth-to-moon
end
Of course, we could have written these tests differently: the
middle test could have been written as
moon-weight(150) is (150 * (1 / 6))
or even
Which form we choose is a function of what our goal is in testing:
what kind of changes we expect the program to undergo, which data we
can establish some other way, and so on. Over the course of our study
we will learn a great deal more about testing.
2.5 Data Structures
Pyret makes it easy to define new kinds of data. For instance, suppose
we have recently taken on a job as a zoo-keeper, and need to maintain
inventories of animals. First, we might make a new datatype to
represent the different kinds of animals:
data Animal:
| boa(name, length)
| armadillo(name, is-alive)
end
This says that there are two kinds of animals, boas and
armadillos. Each has two additional pieces of information,
known as fields: a name and length in one case, and a name and
living status in the other.Armadillos have a high
mortality rate. As the old political saying goes, “In Texas there
ain’t nothing in the middle of the road except a yellow line and a
dead armadillo”, attributed to Jim Hightower and others.
First let’s see how to make instances of these data. We simply use the
name given in the data definition as the name of a function, with as
many arguments as the data definition indicates:
s1 = boa("Slithers", 10)
s2 = boa("Monty", 5)
s3 = boa("Feathered", 15)
a1 = armadillo("Houston", true)
a2 = armadillo("Austin", false)
Each of these functions is called a
constructor.Thus boa is a constrictor
constructor.
Now we can write functions over these data. Let’s implement the Texan
highway function, which renders armadillos defunct. The critical thing
we need is a way of telling which kind of Animal we have, and
extracting its fields. Here is one way:
fun run-over-1(a):
cases (Animal) a:
| boa(_, _) => raise("doesn't apply to boas")
| armadillo(n, _) => armadillo(n, false)
end
where:
run-over-1(a1) is armadillo("Houston", false)
run-over-1(a2) is armadillo("Austin", false)
run-over-1(s1) raises ""
end
2.5.1 Dangerous Field Accesses
There is actually another way to extract fields: using the
dot-operator. Here it is at work:
fun run-over-2(a):
armadillo(a.name, false)
end
However, this version is dangerous, because it extracts the
name field of every argument, irrespective of whether or not
it’s a boa! Thus, these tests pass:
run-over-2(a1) is armadillo("Houston", false)
run-over-2(a2) is armadillo("Austin", false)
but so does this one:
run-over-2(s1) is armadillo("Slithers", false) # WRONG!
Therefore, we should use direct access to fields carefully, only after
making sure we’re working with the kind of data we
expect. However, this is still susceptible to the same error in case
the data definition grows in the future. Therefore, it is safer to
guard field accesses. There are two ways to do it:
By using a conditional. Each variant of a data definition
automatically creates a predicate that checks for whether a value
belongs to that variant. Thus, the function is-armadillo
determines whether or not a value was constructed using
armadillo.
However, writing these checks everywhere can be tedious. Another
approach is to use Annotations.
2.6 Annotations
When we define a function, we have in mind some kind of data the
function is supposed to be supplied, and some kind of datum it will
produce. Though Pyret does not require it, it would be nice to write
down these expectations in the program itself; if we do, as a bonus,
Pyret will check them for us. There are many places in a Pyret program
where we can place annotations, but the most common are on the
parameters of a function, and on the kind of value it will
return. Here are annotations on the preceding function:
fun moon-weight(earth-weight :: Number) -> Number:
earth-weight * earth-to-moon
end
The annotation earth-weight :: Number says that the parameter
must be a number, while -> Number after the parameters and
before the body says that the function computes a number, too.
Do Now!
Change these annotations to something else and see what happens. For
instance, try using String instead of Number in each
of the annotation positions.
2.6.1 Annotation Refinement
Returning to our dangerous use of the dot operator, there is actually
one way to use the it safely in this setting, which is to write an
annotation in the function header indicating what kind of data we
expect. However, the annotation a :: Animal is too weak,
because we’re trying to avoid boas.
Pyret allows you to refine an annotations, and datatypes provide
predicates to identify the different kinds of constructed data,
by prefixing the constructor name with is-. For instance,
is-armadillo is true only of values constructed with
armadillo, not boa or anything else. Thus, the
annotation a :: Animal%(is-armadillo) says that a can
only be bound to Animal values that further pass the
is-armadillo predicate:
fun run-over-3(a :: Animal%(is-armadillo)):
armadillo(a.name, false)
where:
run-over-3(a1) is armadillo("Houston", false)
run-over-3(a2) is armadillo("Austin", false)
run-over-3(s1) raises ""
end