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. The
frame operator draws a small
black frame around the image.
# 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.
In programming, a function takes one or more (configuration) parameters and uses them to produce a result.
Strategy: Creating Functions From Expressions
If we have multiple concrete expressions that are identical except for a couple of specific data values, we create a function with the common code as follows:
Write down at least two expressions showing 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
30, the use of
aboveto stack the rectangles) and which are changing (i.e., the stripe colors).
For each changing part, give it a name (say
bottom), which will be the parameter that stands for that part.
- Rewrite the examples to be in terms of these parameters. For example:
frame( above(rectangle(120, 30, "solid", top), above(rectangle(120, 30, "solid", middle), rectangle(120, 30, "solid", bottom))))
Name the function something suggestive: e.g.,
- Write the syntax for functions around the expression:
fun <function name>(<parameters>): <the expression goes here> endwhere the expression is called the body of the function. (Programmers often use angle brackets to say “replace with something appropriate”; the brackets themselves aren’t part of the notation.)
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
Why does the function body have only one expression, when before we had a separate one for each flag?
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
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
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 function
(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
What if we made a mistake, and tried to call the function as follows:
three-stripe-flag(50, "blue", "red")
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
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
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
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 :: String, mid :: String, bot :: String): frame( above(rectangle(120, 30, "solid", top), above(rectangle(120, 30, "solid", mid), rectangle(120, 30, "solid", bot)))) end
Notice that the notation here is similar to what we saw in contracts
within the documentation: the parameter name is followed by a
::) and a type name (so far, one of
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
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 :: String, mid :: String, bot :: String) -> Image: frame( above(rectangle(120, 30, "solid", top), above(rectangle(120, 30, "solid", mid), rectangle(120, 30, "solid", bot)))) 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.
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
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.
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, where DRY means "don’t repeat yourself".
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,
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/6This will be the body, i.e., the expression inside the function.
Come up with a suggestive name for the function: e.g.,
- 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
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.
whereclause that records the examples. For instance, our
moon-weightfunction can be modified to read:
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—
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 large collections of examples—
For our purposes, we are writing examples as part of the process of making sure we understand the problem. It’s always a good idea to make sure you understand the question before you start writing code to solve a problem. Examples are a nice intermediate point: you can sketch out the relevant computation on concrete values first, then worry about turning it into a function. If you can’t write the examples, chances are you won’t be able to write the function either. Examples break down the programming process into smaller, manageable steps.
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
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: num-pens * (0.25 + (string-length(message) * 0.02)) end
"to begin and end it, like so:
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
whereexamples, we also want to include special yet valid cases that the function might have to handle, such as an empty message.
pen-cost(5, "") is 5 * 0.25
is. The expression for what the function returns doesn’t have to match the body expression; it simply has to evaluate to the same value as you expect the example to produce. Sometimes, we’ll find it easier to just write the expected value directly. For the case of someone ordering no pens, for example, we’d include:
pen-cost(0, "bears") is 0
isshould summarize the computation or the answer in some meaningful way. Most important? Do not write the function, run it to determine the answer, then put that answer on the right side of the
is! Why not? Because the examples are meant to give some redundancy to the design process, so that you catch errors you might have made. If your function body is incorrect, and you use the function to generate the example, you won’t get the benefit of using the example to check for errors.
We’ll keep returning to this idea of writing good examples. Don’t worry if you still have questions for now. Also, for the time being, we won’t worry about nonsensical situations like negative numbers of pens. We’ll get to those after we’ve learned additional coding techniques that will help us handle such situations properly.
Do Now!We could have combined our two special cases into one example, such as
pen-cost(0, "") is 0Does doing this seem like a good idea? Why or why not?
This chapter has introduced the idea of a function. Functions play a
key role in programming: they let us configure computations with
different concrete values at different times. The first time we
compute the cost of pens, we might be asking about
10 pens that say
"Welcome". The next time, we might be asking about
100 pens that say
"Go Bears!". The core computation is the same in both cases, so we
want to write it out once, configuring it with different concrete
values each time we use it.
We’ve covered several specific ideas about functions:
We showed the
funnotation for writing functions. You learned that a function has a name (that we can use to refer to it), one or more parameters (names for the values we want to configure), as well as a body, which is the computation that we want to perform once we have concrete values for the parameters.
We showed that we should include examples with our functions, to illustrate what the function computes on various specific values. Examples go in a
whereblock within the function.
We showed that we can use a function by providing concrete values to configure its parameters. To do this, we write the name of the function we want to use, followed by a pair of parenthesis around comma-separated values for the parameters. For example, writing the following expression (at the interactions prompt) will compute the cost of a specific order of pens:
We discussed that if we define a function in the definitions pane then press Run, Pyret will make an entry in the directory with the name of the function. If we later use the function, Pyret will look up the code that goes with that name, substitute the concrete values we provided for the parameters, and return the result of evaluating the resulting expression. Pyret will NOT produce anything in the interactions pane for a function definition (other than a report about whether the examples hold).
There’s much more to learn about functions, including different reasons for creating them. We’ll get to those in due course.