5 From Repeated Expressions to Functions
5.1 Example: Similar Flags
Consider the following two expressions to draw the flags of Armenia and Austria (respectively). These two countries have the same flag, just with different colors:
# Lines starting with # are comments for human readers. # Pyret ignores everything on a line after #. # 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"))))
Rather than write this program twice, it would be nice to write the common expression only once, then just change the colors to generate each flag. Concretely, we’d like to have a custom operator such as three-stripe-flag that we could use as follows:
# armenia three-stripe-flag("red", "blue", "orange") # austria three-stripe-flag("red", "white", "red")
In this program, we provide three-stripe-flag only with the information that customizes the image creation to a specific flag. The operation itself would take care of creating and aligning the rectangles. We want to end up with the same images for the Armenian and Austrian flags as we would have gotten with our original program. Such an operator doesn’t exist in Pyret: it is specific only to our application of creating flag images. To make this program work, then, we need the ability to add our own operators (henceforth called functions) to Pyret.
5.2 Defining Functions
In programming, a function takes one or more (configuration) parameters and uses them to produce a result. Specifically, the way we create a function is to
Write down some examples of the desired computation (in this case, the expressions that produce the Armenian and Austrian flags).
Identify which parts are fixed (i.e., the creation of rectangles with dimensions 120 and 30, the use of above to stack the rectangles) and which are changing (i.e., the stripe colors).
For each changing part, give it a name (say top, middle, and bottom), which will be the parameter that stands for that part.
- Rewrite the examples to be in terms of these parameters:
frame( above(rectangle(120, 30, "solid", top), above(rectangle(120, 30, "solid", middle), rectangle(120, 30, "solid", bottom))))
Do Now!
Why is there now only one expression, when before we had a separate one for each flag?
We have only one expression because the whole point was to get rid of all the changing parts and replace them with parameters. Name the function something suggestive: e.g., three-stripe-flag.
- Write the syntax for functions around the expression:
fun <function name>(<parameters>): <the expression goes here> end
where the expression is called the body of the function.
fun three-stripe-flag(top, middle, bot): frame( above(rectangle(120, 30, "solid", top), above(rectangle(120, 30, "solid", middle), rectangle(120, 30, "solid", bot)))) end
With this function in hand, we can write the following two expressions to generate our original flag images:
three-stripe-flag("red", "blue", "orange") three-stripe-flag("red", "white", "red")
When we provide values for the parameters of a function to get a result, we say that we are calling the function. We use the term call for expressions of this form.
If we want to name the resulting images, we can do so as follows:
armenia = three-stripe-flag("red", "blue", "orange") austria = three-stripe-flag("red", "white", "red")
(Side note: Pyret only allows one value per name in the directory. If your file already had definitions for the names armenia or austria, Pyret will give you an error at this point. You can use a different name (like austria2) or comment out the original definition using #.)
5.2.1 How Functions Evaluate
So far, we have learned three rules for how Pyret processes your program:
If you write an expression, Pyret evaluates it to produce its value.
If you write a statement that defines a name, Pyret evaluates the expression (right side of =), then makes an entry in the directory to associate the name with the value.
If you write an expression that uses a name from the directory, Pyret substitutes the name with the corresponding value.
Now that we can define our own functions, we have to consider two more cases: what does Pyret do when you define a function (using fun), and what does Pyret do when you call a functiom (with values for the parameters)?
When Pyret encounters a function definition in your file, it makes an entry in the directory to associate the name of the function with its code. The body of the function does not get evaluated at this time.
When Pyret encounters a function call while evaluating an expression, it replaces the call with the body of the function, but with the parameter values substituted for the parameter names in the body. Pyret then continues to evaluate the body with the substituted values.
As an example of the function-call rule, if you evaluate
three-stripe-flag("red", "blue", "orange")
Pyret starts from the function body
frame( above(rectangle(120, 30, "solid", top), above(rectangle(120, 30, "solid", middle), rectangle(120, 30, "solid", bot))))
substitutes the parameter values
frame( above(rectangle(120, 30, "solid", "red"), above(rectangle(120, 30, "solid", "blue"), rectangle(120, 30, "solid", "orange"))))
then evaluates the expression, producing the flag image.
Note that the second expression (with the substituted values) is the same expression we started from for the Armenian flag. Substitution restores that expression, while still allowing the programmer to write the shorthand in terms of three-stripe-flag.
5.2.2 Type Annotations
What if we made a mistake, and tried to call the function as follows:
three-stripe-flag(50, "blue", "red")
Do Now!
What do you think Pyret will produce for this expression?
The first parameter to three-stripe-flag is supposed to be the color of the top stripe. The value 50 is not a string (much less a string naming a color). Pyret will substitute 50 for top in the first call to rectangle, yielding the following:
frame( above(rectangle(120, 30, "solid", 50), above(rectangle(120, 30, "solid", "blue"), rectangle(120, 30, "solid", "red"))))
When Pyret tries to evaluate the rectangle expression to create the top stripe, it generates an error that refers to that call to rectangle.
If someone else were using your function, this error might not make sense: they didn’t write an expression about rectangles. Wouldn’t it be better to have Pyret report that there was a problem in the use of three-stripe-flag itself?
As the author of three-stripe-flag, you can make that happen by annotating the parameters with information about the expected type of value for each parameter. Here’s the function definition again, this time requiring the three parameters to be strings:
fun three-stripe-flag(top-color :: String, mid-color :: String, bot-color :: String): frame( above(rectangle(120, 30, "solid", top-color), above(rectangle(120, 30, "solid", mid-color), rectangle(120, 30, "solid", bot-color)))) end
Notice that the notation here is similar to what we saw in contracts within the documentation: the parameter name is followed by a double-colon (::) and a type name (so far, one of Number, String, or Image).Putting each parameter on its own line is not required, but it sometimes helps with readability.
Run your file with this new definition and try the erroneous call again. You should get a different error message that is just in terms of three-stripe-flag.
It is also common practice to add a type annotation that captures the type of the function’s output. That annotation goes after the list of parameters:
fun three-stripe-flag(top-color :: String, mid-color :: String, bot-color :: String) -> Image: frame( above(rectangle(120, 30, "solid", top-color), above(rectangle(120, 30, "solid", mid-color), rectangle(120, 30, "solid", bot-color)))) end
Note that all of these type annotations are optional. Pyret will run your program whether or not you include them. You can put type annotations on some parameters and not others; you can include the output type but not any of the parameter types. Different programming languages have different rules about types.
We will think of types as playing two roles: giving Pyret information that it can use to focus error messages more accurately, and guiding human readers of programs as to the proper use of user-defined functions.
5.2.3 Documentation
Imagine that you opened your program file from this chapter a couple of months from now. Would you remember what computation three-stripe-flag does? The name is certainly suggestive, but it misses details such as that the stripes are stacked vertically (rather than horizontally) and that the stripes are equal height. Function names aren’t designed to carry this much information.
Programmers also annotate a function with a docstring, a short, human-language description of what the function does. Here’s what the Pyret docstring might look like for three-stripe-flag:
fun three-stripe-flag(top :: String, middle :: String, bot :: String) -> Image: doc: "produce image of flag with three equal-height horizontal stripes" frame( above(rectangle(120, 30, "solid", top), above(rectangle(120, 30, "solid", middle), rectangle(120, 30, "solid", bot)))) end
While docstrings are also optional from Pyret’s perspective, you should always provide one when you write a function. They are extremely helpful to anyone who has to read your program, whether that is a co-worker, grader…or yourself, a couple of weeks from now.
5.3 Functions Practice: Moon Weight
100 * 1/6 150 * 1/6 90 * 1/6
In the case of the flags, we noticed we had written essentially the same expression more than once. Here, we have a computation that we expect to do multiple times (once for each astronaut). It’s boring to write the same expression over and over again. Besides, if we copy or re-type an expression multiple times, sooner or later we’re bound to make a transcription error.This is an instance of the DRY principle.
Let’s remind ourselves of the steps for creating a function:
Write down some examples of the desired calculation. We did that above.
Identify which parts are fixed (above, * 1/6) and which are changing (above, 100, 150, 90...).
For each changing part, give it a name (say earth-weight), which will be the parameter that stands for it.
- Rewrite the examples to be in terms of this parameter:
earth-weight * 1/6
This will be the body, i.e., the expression inside the function. Come up with a suggestive name for the function: e.g., moon-weight.
- Write the syntax for functions around the body expression:
fun moon-weight(earth-weight): earth-weight * 1/6 end
- Remember to include the types of the parameter and output, as well as the documentation string. This yields the final function:
fun moon-weight(earth-weight :: Number) -> Number: doc:" Compute weight on moon from weight on earth" earth-weight * 1/6 end
5.4 Documenting Functions with Examples
In each of the functions above, we’ve started with some examples of what we wanted to compute, generalized from there to a generic formula, turned this into a function, and then used the function in place of the original expressions.
Now that we’re done, what use are the initial examples? It seems tempting to toss them away. However, there’s an important rule about software that you should learn: Software Evolves. Over time, any program that has any use will change and grow, and as a result may end up producing different values than it did initially. Sometimes these are intended, but sometimes these are a result of mistakes (including such silly but inevitable mistakes like accidentally adding or deleting text while typing). Therefore, it’s always useful to keep those examples around for future reference, so you can immediately be alerted if the function deviates from the examples it was supposed to generalize.
fun moon-weight(earth-weight :: Number) -> Number: doc:" Compute weight on moon from weight on earth" earth-weight * 1/6 where: moon-weight(100) is 100 * 1/6 moon-weight(150) is 150 * 1/6 moon-weight(90) is 90 * 1/6 end
Do Now!
Check this! Change the formula—for instance, replace the body of the function with earth-weight * 1/3—and see what happens. Pay attention to the output from CPO: you should get used to recognizing this kind of output.
Do Now!
Now, fix the function body, and instead change one of the answers—e.g., write moon-weight(90) is 90 * 1/3—and see what happens. Contrast the output in this case with the output above.
Of course, it’s pretty unlikely you will make a mistake with a
function this simple (except through a typo). After all, the examples
are so similar to the function’s own body. Later, however, we will see
that the examples can be much simpler than the body, and there is a real chance
for things to get inconsistent. At that point, the examples become invaluable
in making sure we haven’t made a mistake in our program. In fact, this is so
valuable in professional software development that good programmers
always write down such examples—
5.5 Functions Practice: Cost of pens
Let’s create one more function, this time for a more complicated example. 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 (we’ll count spaces between words as characters).
Following our steps to create a function once again, let’s start by writing two concrete expressions that do this computation.
# ordering 3 pens that say "wow" 3 * (0.25 + (string-length("wow") * 0.02)) # ordering 10 pens that say "smile" 10 * (0.25 + (string-length("smile") * 0.02))
These examples introduce a new built-in function called string-length. It takes a string as input and produces the number of characters (including spaces and punctuation) in the string. These examples also show an example of working with numbers other than integers.Pyret requires a number before the decimal point, so if the “whole number” part is zero, you need to write 0 before the decimal. Also observe that Pyret uses a decimal point; it doesn’t support conventions such as “0,02”.
fun pen-cost(num-pens :: Number, message :: String): num-pens * (0.25 + (string-length(message) * 0.02)) end
fun pen-cost(num-pens :: Number, message :: String) -> Number: num-pens * (0.25 + (string-length(message) * 0.02)) end
fun pen-cost(num-pens :: Number, message :: String) -> Number: doc: ```total cost for pens, each 25 cents plus 2 cents per message character``` num-pens * (0.25 + (string-length(message) * 0.02)) end
fun pen-cost(num-pens :: Number, message :: String) -> Number: 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 3 * (0.25 + (string-length("wow") * 0.02)) pen-cost(10, "smile") is 10 * (0.25 + (string-length("smile") * 0.02)) end
pen-cost(5, "") is 5 * 0.25
pen-cost(0, "bears") is 0
Do Now!
We could have combined our two special cases into one example, such aspen-cost(0, "") is 0Does doing this seem like a good idea? Why or why not?