Class summary: Function Evaluation and Practice
Copyright (c) 2017 Kathi Fisler
1 Warm-Up: Ticket Prices
A sports stadium offers special ticket pricing for large groups. The first 10 tickets cost $15 each, and additional tickets cost $10 each. Develop a function ticket-price that takes a number of tickets and produces the total cost of that many tickets.
Identify the name, parameters, body, and header of your definition.
2 How Functions Evaluate
We have talked about how expressions evaluate: from the innermost-expression out, with definitions extending the dictionary. But what happens when we add our own functions? How do programs with functions evaluate? We’ll work this first with a small function, then revisit on the ticket-price function.
2.1 Information Tracked During Evaluation
As a reminder, Pyret tracks two pieces of information at all times:
The dictionary of known names
(While evaluating) the current (sub) expression being evaluated.
As we look at how functions affect evaluation, our goal is to be clear on how each of defining and using functions affects these two pieces of information.
2.2 Substitute Arguments into Function Bodies
Assume that we have the following definition, and want to evaluate the expression 5 + cube(2).
# contents of definitions window |
fun cube(n :: Number): |
n * n * n |
where: |
cube(1) is 1 |
cube(4) is 64 |
end |
Evaluation still goes inside out (and left-to-right), but when we get to call to a user-defined function, we substitute the argument into the body of the function, then continue evaluating. Here’s what the evaluation looks like step-by-step:
5 + cube(2) |
|
5 + (2 * 2 * 2) |
|
5 + 8 |
|
13 |
In the 2nd line, we replaced the call to cube{2} with the body of cube, but with each use of n replaced with the argument 2. Once the substitution occurs, evaluation continues using the same "inside-out" rule that we had before.
2.3 Handling Expressions as Arguments
What if we had the program cube(3 + 1). Would the first three steps look like:
cube(3 + 1) |
|
cube(4) |
|
4 * 4 * 4 |
or
cube(3 + 1) |
|
(3 + 1) * (3 + 1) * (3 + 1) |
|
4 * 4 * 4 |
or
cube(3 + 1) |
|
(3 + 1) * (3 + 1) * (3 + 1) |
|
4 * (3 + 1) * (3 + 1) |
The first sequence is correct. By the inside-out rule, Pyret will evaluate the argument (3+1) before it substitutes in the body of the function.
3 Evaluating Functions and If-Expressions
Combining functions and if-expressions doesn’t introduce anything new, but it does reinforce the rules we’ve already learned.
Go back to the ticket-price function and show the evaluation steps for the expression that follows the function definition.
fun ticket-price(num-tix): |
if (num-tix > 10): |
(15 * 10) + ((num-tix - 10) * 10) |
else: |
num-tix * 15 |
end |
where: |
ticket-price(10) is 150 |
ticket-price(2) is 30 |
ticket-price(12) is 170 |
end |
|
ticket-price(12) |
ticket-price(12)
if (12 > 10): (15 * 10) + ((12 - 10) * 10) else: 12 * 15 end
if true: (15 * 10) + ((12 - 10) * 10) else: 12 * 15 end
(15 * 10) + ((12 - 10) * 10)
150 + ((12 - 10) * 10)
150 + (2 * 10)
150 + 20
170
4 How Functions Affect the Dictionary
We have talked about how definitions impact the dictionary of known names within Pyret. How do functions affect the dictionary? Consider the following definitions window contents:
outside = 100 |
|
fun colored-square(color): |
square(outside, "solid", color) |
end |
Running this program adds two names to the dictionary: outside and colored-square. The fun construct creates dictionary entries, just as = does.
5 Defining Versus Calling Functions
Does a red square get created when we run the definitions window from the previous section? No. All the fun construct does is augment the dictionary with a new operator.
We only get a red square when we use or call the function. For example, if we type
colored-square("red") |
in the Interactions window, Pyret evaluates the expression with the following sequence:
colored-square("red") |
|
square(outside, "solid", "red") |
|
# which produces the image |
It is essential to understand that defining and calling functions are two separate actions. Defining a function augments the dictionary, but the function body does not get evaluated when a function is defined. Calling a function evaluates its body.
A function gets defined only once, but it may be called many times, which means its body may be evaluated many times. To see this, consider the following sequence:
cube(2) + cube(3) |
|
(2 * 2 * 2) + cube(3) |
|
8 + cube(3) |
|
8 + (3 * 3 * 3) |
|
8 + 27 |
|
35 |
The distinction between defining and calling functions is a common stumbling block when people learn programming. Keep the dictionary in mind: we define functions in order to name computations that we’ll use multiple times. Naming is separate from using.
6 More Functions Practice
Here are several problems with which to practice both defining and evaluating functions. Make sure to use the Design Recipe while designing each one, thinking about a good set of examples.
Develop the function image-classify, which consumes an image and conditionally produces "tall" if the image is taller than wide, "wide" if it is wider than tall, or "square" if its width and height are the same.
[Problem credit: How to Design Programs, 2nd edition]
Develop the function custom-message, which takes the name of a person and a discount to offer to that person. The function produces a string consisting of a message telling the person what discount they can get by shopping today. Here’s an example:
custom-message("Kathi", 10) is |
"Hi Kathi! Shop today and get a 10% discount" |
Pyret has a built-in function called to-string that converts any value into a string. You may find that helpful here.
You want to split the cost of a pizza with friends. Each of you will pay for the number of slices you ate, as well as your share of the tip on those slices. Assume a pizza costs $12 and has 8 slices. Write a function pizza-share that consumes the number of slices that you ate and returns your share of the cost of the pizza including a 10% tip.
fun pizza-cost(slices): |
doc: "compute cost of given number of pizza slices, plus 10% tip" |
(12 * (slices / 8)) * 1.10 |
where: |
pizza-cost(8) is 12 * 1.10 |
pizza-cost(4) is 6 * 1.10 |
end |
This function actually has two tasks in it —
fun tip(amt): |
amt * 1.10 |
where: |
tip(1) is 1.10 |
tip(2) is 2.20 |
tip(5) is 5.50 |
end |
|
fun pizza-cost(slices): |
doc: "compute cost of given number of pizza slices, plus tip" |
tip(16 * (slices / 8)) |
where: |
pizza-cost(8) is 13.20 |
pizza-cost(4) is 6.60 |
end |
Note that we also write examples for helper functions such as tip. Why? Two reasons: (1) we still want examples to confirm that the function is working properly, and (2) if we get the wrong answer from pizza-cost and have not checked tip, we won’t know which function contains the error.
Recall the gradebook programs we wrote on tables last week. Develop a function called trajectory that takes in a student’s grades on the first two exams and produces a string that summarizes the direction of the student’s exam grades. If the two grades are within 5 points of each other, the direction is "steady". Otherwise, if the second exam is higher, the direction is "improving"; if the first exam is higher, the direction is "declining".
Load up the gradebook table from last week, and use the trajectory function to extend the gradebook with a new column that reports the trajectory.
How good were your test cases for trajectory? Open this Pyret file, which contains three solutions to this problem (submitted by students in a previous course). Did your examples pick up that there were errors in these solutions? To find out, paste your where clause into each of these definitions (changing the name of the trajectory function to match which function you are testing). Do any of your examples fail? If not, then your examples didn’t cover the situation in which the solution was wrong. Add examples until you find one that shows the error in the solution, then try to describe more generally what situation had the error.
spring: Mar 20 - Jun 19
summer: Jun 20 - Sept 21
fall: Sept 22 - Dec 20
winter: Dec 21 - Mar 19
Develop a function called season that consumes a month (as a String) and a day (a Natural). The function produces the season (a String) for that date.
Note: there are different ways to break this problem down into functions and helper functions. Spend some time thinking about how you might do this, rather than diving in to start coding.
[Problem credit: Glynis Hamel, WPI CS]