Class Summary: How Programs Evaluate
Copyright (c) 2017 Kathi Fisler
The textbook discusses how expressions evaluate, but not how definitions evaluate or the relationship between the definitions and interactions windows.
Now that you’re learning how to write programs, you also need to understand how Pyret will compute the results of those programs.
1 Evaluating Expressions
We say that expressions evaluate to values. The result computed by an expression is its value. The process by which an expression is computed into a value is called evaluation.
You probabaly already have an intuition for evaluation from arithmetic. In what order do you perform the operations when you evaluate
3 * (4 + (6 - 1)) |
? We do 6 - 1 first, then we add 4, then we multiply by 3. We work from the inside out, evaluating the innermost computation, replacing the expression with its value, then moving out to the next innermost expression.
The same rule—
overlay-xy(circle(15, "solid", "blue"), |
-25, -25, |
circle(15 * 2, "outline", "black")) |
The expression for the blue circle evaluates first, then the expression for the black circle, then the call to overlay-xy, which produces the final image.
2 Evaluating Definitions
Imagine that you had the following contents in the definitions window (the one on the left):
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)) |
|
# what would happen if we entered each of the following into the |
# interactions window BEFORE pressing Run? |
|
circle(15, "solid", "red") |
circle(SCOOP-SIZE, "solid", "red") |
cone = triangle(30, "solid", "blue") |
|
# what would happen if we entered each of the following into the |
# interactions window AFTER pressing Run? |
|
SCOOP-SIZE |
SCOOP |
cone = triangle(30, "solid", "blue") |
You can try these out yourself to check your answers!
Now that you see what happens, let’s understand why.
2.1 The Known Names of Operations and Values
Under the hood, Pyret maintains what you can think of as a known names that maps names to values and operations. When you first start a Pyret window, the known names contains entries such as:
* ---> the multiplication operator |
/ ---> the division operator |
string-length ---> the operator to compute the length of a string |
In the interactions window, you can write expressions using operators that are in the known names (Pyret also knows the number and string values, so you can use them in expressions too).
Now let’s say you want to make a circle. The circle operation isn’t in the default known names (Pyret keeps the known names small, unless you tell it otherwise). When you run
include image |
You are telling Pyret "add the image operations to the known names". So now the known names looks like:
* ---> the multiplication operator |
/ ---> the division operator |
string-length ---> the operator to compute the length of a string |
circle ---> the operation to create circles |
... |
You can also add to the known names by writing definitions that associate names with values. So if you write
red-circ = circle(50, "solid", "red") |
Pyret extends the known names to look like:
* ---> the multiplication operator |
/ ---> the division operator |
string-length ---> the operator to compute the length of a string |
circle ---> the operation to create circles |
... |
red-circ ---> the value for a solid red circle with radius 50 |
2.2 Using the Known Names
Now assume you wrote the following:
scale(10, red-circ) |
How does Pyret evaluate this expression? First, it checks that the operator (scale) exists in the known names (this got added when you did include image). It then evaluates the operands from left to right. It evaluates 10, which is already a value. Then it evaluates red-circ. red-circ is a name, so Pyret goes to the known names to look it up. The known names has an entry for red-circ, so Pyret grabs the associated value. Finally, Pyret uses the scale operation from the known names to compute a value.
When you use a name in your program, whether as an operator, and operand, or just as a value at the prompt, Pyret goes to the known names to look it up. If you ask for something that isn’t in the known names, then Pyret gives you the error saying that "this identifier is unbound". "Identifier" is the technical term for a name in a programming language. "Unbound" is the technical term for "not in the known names".
2.3 Evaluating Files (and Interactions)
Let’s put it all together. Go back to the ice cream cone program 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)) |
When you hit the Run button, Pyret takes the following steps:
It resets the known names to the initial one (with just the operators on numbers, strings, and other basic stuff we haven’t learned yet).
It goes through the definitions window, one item at a time, and evaluates each item:
If the item is an include statement, Pyret extends the known names.
If the item is a definition, Pyret evaluates the expression being named, then extends the known names with the defined name and the computed value.
If the item is an expression, Pyret evalutes the expression and displays the result in the interactions window.
While evaluating expressions, Pyret looks up values of names that are used in expressions.
Once all the items in the file have been processed, Pyret waits for you to enter expressions in the interactions window. When you do, it evaluates them (using the known names when needed) and displays the result of each expression.
To check your understanding of these rules, ask yourself what happens in each of the following situations:
You run the ice-cream-cone program, then type scoop in the interactions window
You run the ice-cream-cone program, then type "scoop" in the interactions window
You run the ice-cream-cone program, then type scoop-size + 5 in the interactions window
You run the ice-cream-cone program, then type scoop-size + cone-size in the interactions window
You enter flavor = "strawberry" in the interactions window, hit run, then enter flavor in the interactions window.
You run the ice-cream-cone program, then type scoop-size = 10 in the interactions window
The last question makes a key point: you can’t give two definitions to the same name. You can edit your file (with a new value for name) and run it again, but you can’t have multiple definitions for the same name. What if you want to update the value in the known names while the program is running? For now, you can’t. Being able to do this complicates all sorts of things, so we will get to updating the known names much later in the course (this may seem odd to those who have programmed before, but there are good reasons for this which we will explain when the class as a whole gets to that point).