Evaluation and functions

How Pyret evaluates expressions

Imagine that you had the following contents in the definitions window:

include image

SCOOP-SIZE = 15
cone = flip-vertical(triangle(SCOOP-SIZE * 2, "solid", "tan"))
overlay-xy(circle(SCOOP-SIZE, "solid", "pink"),
  0, 25,
  overlay-xy(circle(SCOOP-SIZE, "solid", "green"),
    0, 25,
    cone))

How will Pyret evaluate this program?

Pyret processes the expressions one at a time from top to bottom.

When Pyret encounters something of the form name = expr, it creates an entry in an internal dictionary. The dictionary maps names to values. Thus, Pyret evaluates the expression on the right of the =, then makes a dictionary entry to associate the name with that value.

When Pyret encounters an expression that has other expressions nested within it, it evaluates the sub-expressions from left-to-right and innermost to outermost. Thus, in the flip-vertical expression (that creates the cone), the first expression to evaluate is SCOOP-SIZE (which Pyret looks up in the dictionary), then Pyret computes 15 * 2, then it creates the triangle, then flips the triangle.

In the longer overlay-xy expression, the pink circle gets created before the green circle (by the left-to-right rule).

You may have noticed that SCOOP-SIZE is all in caps, while cone is in lowercase. Conceptually, SCOOP-SIZE is a key concept in the program (which we plan to use multiple times), whereas cone is just naming an intermediate computation for readability. We use this naming convention to help us remember the respective roles of the names in the program. What happens if we change SCOOP-SIZE?

What does the “Run” button do?

When you press the Run button, Pyret erases the dictionary, then re-processes the definitions window from the beginning. Any dictionary entries that you made only in the interactions (right) window disappear.

Expressions and statements

We’ve already seen that there’s a difference between

SCOOP-SIZE = 15

and

SCOOP-SIZE * 2

and their impact on the interactions window: the first adds to the dictionary, while the second displays a value. A piece of a program that changes how future expressions will evaluate is called a statement. Creating a name meets this definition, since expressions will yield errors or not depending on whether a name appears in the dictionary. Expressions perform computations without changing the information that Pyret maintains for running future expressions.

This distinction will become more meaningful later in the course, but we’ll use the terminology to get used to it now.

Functions

Consider programs to draw a couple of slightly different striped flags.

armenia =
  frame(
    above(rectangle(120, 30, "solid", "red"),
      above(rectangle(120, 30, "solid", "blue"),
        rectangle(120, 30, "solid", "orange"))))

austria =
  frame(
    above(rectangle(120, 30, "solid", "red"),
      above(rectangle(120, 30, "solid", "white"),
        rectangle(120, 30, "solid", "red"))))

These flags have some things in common! It might be nice to be able to avoid writing very similar code twice. We could try writing it just once, then changing the colors for each flag. In order to do that, we can write a function.

What we’re going to do is to find the places that the two flags differ, then give names to those places. Here, the middle and bottom colors are different, but everything else stays the same. Because there might be other flags with similar patterns (and there are!), we can assume that the top color might change as well. Let’s call these colors top, middle, and bottom. We’re going to use these names to create a new operator, called three-stripe-flag. We do that like this:

fun three-stripe-flag(top, middle, bottom):
...
end

three-stripe-flag is the name of the function. We call its inputs (top, middle, and bottom) its parameters.

What replaces the ...? We can copy the expression from armenia, but replace the colors with the names we came up with:

fun three-stripe-flag(top, middle, bottom):
  doc: "produce a flag with 3 horizontal stripes"
  frame(
    above(rectangle(120, 30, "solid", top),
      above(rectangle(120, 30, "solid", middle),
        rectangle(120, 30, "solid", bottom))))
end

The expression inside the function is called the function’s body.

We can call it just like any other operator:

armenia = three-stripe-flag("red", "blue", "orange")
austria = three-stripe-flag("red", "white", "red")
gabon = three-stripe-flag("green", "yellow", "blue")

What if we make a mistake, and try to use the function as follows:

> three-stripe-flag(50, "black", "white")

What do you think should happen? 50 is not a string (much less a string naming a color). Pyret will give an error on the first rectangle command (remember the order in which we evaluate expressions!). In other words, the error comes up inside the computation that creates the flag.

It would be much more helpful if we got the error when we tried to use three-stripe-flag. We can annotate the inputs with types telling Pyret what kind of data the function expects to receive:

fun three-stripe-flag(top :: String, middle :: String, bottom :: String):
  doc: "produce a flag with 3 horizontal stripes"
  frame(
    above(rectangle(120, 30, "solid", top),
      above(rectangle(120, 30, "solid", middle),
        rectangle(120, 30, "solid", bottom))))
end

If you try the erroneous call again, you’ll see that the location where the error is reported has changed.

We can also add a type to the output of the function. This will tell anyone calling the function about the contexts in which they can use it. Pyret will complain if the function’s body has the wrong type.

fun three-stripe-flag(top :: String, middle :: String, bottom :: String)
  -> Image:
  doc: "produce a flag with 3 horizontal stripes"
  frame(
    above(rectangle(120, 30, "solid", top),
      above(rectangle(120, 30, "solid", middle),
        rectangle(120, 30, "solid", bottom))))
end