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.
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))))
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> endwhere 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 #.)
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.
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 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.
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.
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/6This 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
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—
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 “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
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 fun notation 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 where block 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 window 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 window 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.