Class summary: Introduction to Functions
Copyright (c) 2017 Kathi Fisler
This material goes with From Repeated Expressions to Functions from the textbook
1 Repeated Computations: Flags Example
Consider programs to draw the flags of Madagascar and Benin. These two countries have the same flag, just with different colors:
madagascar = |
frame( |
beside(rectangle(40, 80, "solid", "white"), |
above(rectangle(70, 40, "solid", "red"), |
rectangle(70, 40, "solid", "green")))) |
|
benin = |
frame( |
beside(rectangle(40, 80, "solid", "green"), |
above(rectangle(70, 40, "solid", "yellow"), |
rectangle(70, 40, "solid", "red")))) |
Rather than write this code twice, it would be nice to write the common code only once, then just change the colors to generate each flag. With what we’ve learned so far, we might do this as follows:
side-color = "white" |
top-color = "red" |
bot-color = "green" |
|
flag= |
frame( |
beside(rectangle(40, 80, "solid", side-color), |
above(rectangle(70, 40, "solid", top-color), |
rectangle(70, 40, "solid", bot-color)))) |
This is unsatisfying, however – what if we were trying to create an image that included both flags at the same time? We can only put one defintion of each color in the Pyret dictionary, so there is no way to change the colors to generate another flag within the same run of the program.
Step back for a moment and think about num-min. If Pyret didn’t provide num-min, could you write that computation by hand?
num1 = 4 |
num2 = 5 |
|
if (num1 < num2): |
num1 |
else: |
num2 |
end |
This has the same problem as our flags example: we’d have to edit the num1 and num2 definitions every time we wanted to compute the min. Fortunately, Pyret gives us this more convenient notation in which we can provide the two numbers as arguments to num-min. Instead, we can write:
num-min(4, 5) |
Wouldn’t it be nice to be able to do something similar for the flags? It would be nice to just write the following:
madagascar = sidebar-flag("white", "red", "green") |
benin= sidebar-flag("green", "yellow", "red") |
This is precisely what you are about to learn how to do. You are going to learn how to add your own operators to Pyret.
2 Defining Your Own Functions
(See the textbook here for details on what a function is – what follows is just summarizing the code from class)
Here is
fun sidebar-flag(side-color, top-color, bot-color): |
doc: "produce image of flag with sidebar and two equal-sized horizontal regions" |
frame( |
beside(rectangle(40, 80, "solid", side-color), |
above(rectangle(70, 40, "solid", top-color), |
rectangle(70, 40, "solid", bot-color)))) |
end |
With this, the two desired flag expressions work fine:
madagascar = sidebar-flag("white", "red", "green") |
benin= sidebar-flag("green", "yellow", "red") |
What if we make a mistake, and try to use the function as follows:
sidebar-flag(50, "solid", "red") |
What do you think should happen? There are two errors here: "solid" is not a color and 50 is not a string (much less a string naming a color). Pyret will give an error on one of the rectangle commands (we’ll see which one in particular in a little while). The point is that the error comes up inside the computation that creates the flag.
Remember what we did with types on table columns though? Once we put types on tables, Pyret gave an error as soon as we tried to create a row with the wrong type of data. Similarly, we can put types on the parameters of a function:
fun sidebar-flag(side-color :: String, top-color :: String, bot-color :: String): |
doc: "produce image of flag with sidebar and two equal-sized horizontal regions" |
frame( |
beside(rectangle(40, 80, "solid", side-color), |
above(rectangle(70, 40, "solid", top-color), |
rectangle(70, 40, "solid", bot-color)))) |
end |
Which of the two errors from our mistaken call does this catch? Just the use of 50. What can you do about the use of "solid"? Nothing just yet, but we’ll get there.
3 Exercises
3.1 Pen-cost
Imagine that you are trying to compute the total cost of an order of pens with slogans (or messages) printed on them. Each pen costs 25 cents, plus an additional 2 cents per character in the message (count spaces as characters for now).
First, let’s write two expressions that do this computation, just to make sure we understand how it should work:
# if ordering 3 pens that say "wow" |
3 * (0.25 + (string-length("wow") * 0.02)) |
|
# if ordering 10 pens that say "smile" |
10 * (0.25 + (string-length("smile") * 0.02)) |
Having a clear idea of the computation you want to do makes it much easier to write a function to do it. How do we figure out a function that would correspond to these two expressions? We find the places where the two functions are different and replace each spot of difference with a unique descriptive name. These names become the parameters of the function.
fun pen-cost(num-pens :: Number, message :: String): |
num-pens * (0.25 + (string-length(message) * 0.02)) |
The initial expressions we developed give rise to our examples. When you write the expected results of the examples, you can either write the final answer or the expression that computed it (each can be useful in certain cases). Here’s the final function:
fun pen-cost(num-pens :: Number, message :: String): |
doc: "total cost for pens, each 25 cents plus 2 cents per message character" |
num-pens * (0.25 + (string-length(message) * 0.02)) |
where: |
pen-cost(3, "wow") is 0.87 |
pen-cost(10, "smile") is 10 * (0.25 + (string-length("smile") * 0.02)) |
What are other good cases to check here? Ordering 0 pens and pens with empty messages are both good cases to check. You don’t have to check negative numbers of pens or messages that aren’t strings though – those fall outside the expected "contract" of the function (we’ll discuss how to properly handle the negatives later).
Once you are comfortable with the idea of writing functions, you would skip this circle-and-label step, and just jump directly to writing the expression with the parameters in place. Use the circle-and-label technique as needed to get yourself to that point.
3.2 Repeated ranges
Revisit our scoville/pepper program. There we had several if/else-if checks with a very similar structure. Introduce a function named in-range that checks whether a given number is within the range (inclusive) bounded by two given numbers. Remember to write examples.
fun in-range(num :: Number, low :: Number, high :: Number) -> Boolean: |
doc: determines whether first num between second and third (inclusive) |
(low <= num) and (num <= high) |
where: |
in-range(0, 0, 5) is true |
in-range(5, 0, 5) is true |
in-range(10, 0, 5) is false |
in-range(-4, 0, 5) is false |
in-range(3, 2, 5) is true |
end |
Questions:
Should we also rewrite the bell-pepper case to use in-range?
If we later decide to make the ranges exclusive of the endpoints, how many places does the original code need to change? How about in the new code?
4 The Design Recipe
As we wrked the following exercises in class, we relied on the the Design Recipe, a sequence of steps for developing functions. We are going to rely heavily on these steps as the course goes on. Even if the steps seem unnecessary now while we are writing simpler functions, it is important for you to develop the habits of the steps now, before we get into harder problems. Homeworks and quizzes will expect that you know and can apply the steps of the design recipe.