On this page:
2.1 Values
2.2 Expressions
2.2.1 Aggregate Values
2.2.2 Precedence
2.3 Functions:   Abstracting Over Expressions
2.4 Examples and Testing:   Predicting Behavior
2.5 Data Structures
2.5.1 Dangerous Field Accesses
2.6 Annotations
2.6.1 Annotation Refinement

2 Programming in Pyret

    2.1 Values

    2.2 Expressions

      2.2.1 Aggregate Values

      2.2.2 Precedence

    2.3 Functions: Abstracting Over Expressions

    2.4 Examples and Testing: Predicting Behavior

    2.5 Data Structures

      2.5.1 Dangerous Field Accesses

    2.6 Annotations

      2.6.1 Annotation Refinement

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:

3

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:

"Hello, Tara!"

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 +.

1 + 2

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

"will." + "i." + "am"

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:

shopping.first

will evaluate to "bread",

shopping.rest

to link("water", empty),

shopping.rest.first

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:

[list: "bread", "water"]

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:

[set: "bread", "water"]

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

4 + 2 - 5 + 1

Pyret wants you to be clearer about your intent: did you mean for this to evaluate as

(4 + 2) - (5 + 1)

(i.e., 0), or

4 + (2 - 5) + 1

(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

moon-weight(150) is 25

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:
  1. 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.

  2. 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