On this page:
5.1 Example:   Similar Flags
5.2 Defining Functions
5.2.1 How Functions Evaluate
5.2.2 Type Annotations
5.2.3 Documentation
5.3 Functions Practice:   Moon Weight
5.4 Documenting Functions with Examples
5.5 Functions Practice:   Cost of pens
5.6 Recap:   Defining Functions

5 From Repeated Expressions to Functions

    5.1 Example: Similar Flags

    5.2 Defining Functions

      5.2.1 How Functions Evaluate

      5.2.2 Type Annotations

      5.2.3 Documentation

    5.3 Functions Practice: Moon Weight

    5.4 Documenting Functions with Examples

    5.5 Functions Practice: Cost of pens

    5.6 Recap: Defining 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

Here’s the end product:

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

While this looks like a lot of work now, it won’t once you get used to it. We will go through the same steps over and over, and eventually they’ll become so intuitive that we won’t even remember that we actually took steps to get from examples to the function: it’ll become a single, natural step.

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:

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)?

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 :: 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 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 :: 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.

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

Suppose we’re responsible for outfitting a team of astronauts for lunar exploration. We have to determine how much each of them will weigh on the Moon’s surface. On the Moon, objects weigh only one-sixth their weight on earth. Here are the expressions for several astronauts (whose weights are expressed in pounds):

100 * 1/6
150 * 1/6
90 * 1/6

As with our examples of the Armenian and Austrian flags, we are writing the same expression multiple times. This is another situation in which we should create a function that takes the changing data as a parameter but captures the fixed computation only once.

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:

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.

Pyret makes this easy to do. Every function can be accompanied by a where clause that records the examples. For instance, our moon-weight function 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

When written this way, Pyret will actually check the answers every time you run the program, and notify you if you have changed the function to be inconsistent with these examples.

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 large collections of examples—called teststo make sure their programs are behaving as they expect.

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.

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”.

The second step to writing a function was to identify which information differs across our two examples. In this case, we have two: the number of pens and the message to put on the pens. This means our function will have two parameters rather than just one.

fun pen-cost(num-pens :: Number, message :: String) -> Number:
  num-pens * (0.25 + (string-length(message) * 0.02))
end

Of course, as things get too long, it may be helpful to use multiple lines:

fun pen-cost(num-pens :: Number, message :: String)
  -> Number:
  num-pens * (0.25 + (string-length(message) * 0.02))
end

If you want to write a multi-line docstring, you need to use ``` rather than " 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

We should also document the examples that we used when creating the function:

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

When writing where examples, 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

Note that our empty-message example has a simpler expression on the right side of 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

The point of the examples is to document how a function behaves on a variety of inputs. What goes to the right of the is should 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 0

Does doing this seem like a good idea? Why or why not?

5.6 Recap: Defining Functions

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:

There’s much more to learn about functions, including different reasons for creating them. We’ll get to those in due course.