On this page:
3.1 Example:   Moon Weight
3.2 Example:   Japanese Flag
3.3 Tests:   Keeping Track of Examples
3.4 Type Annotations
3.5 Defining Functions in Steps

3 From Repeated Expressions to Functions

    3.1 Example: Moon Weight

    3.2 Example: Japanese Flag

    3.3 Tests: Keeping Track of Examples

    3.4 Type Annotations

    3.5 Defining Functions in Steps

3.1 Example: 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. We know how to do this—we saw the expression earlier [REF]—but it’s boring to write it 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. Separately, correcting errors is itself an interesting computer science topic, which we address much later [REF].

When looking at our Moon weight calculations—say

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

we see that there are parts that are “fixed” and parts that are “changing”. The fixed parts are the ones we don’t want to have to repeat; the changing parts are the ones we have no choice about (and want the freedom to vary). It would be nice to make a package that makes this difference clear.

The way we’ll do it is to write a function. A function takes one or more parameters, which are the parts that vary. Specifically, the way we create a function is to
  • Write down some examples of the desired calculation.

  • 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

    Do Now!

    Why is there only one expression, when before we had many?

    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., moon-weight.

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

Wow, that looks like a lot of work! But the end-product is really quite simple:

fun moon-weight(earth-weight): earth-weight * 1/6 end

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.

How do we use this? From Pyret’s point of view, moon-weight is just another operator just like num-expt or overlay. Thus:

moon-weight(100) moon-weight(150) moon-weight(90)

will produce the same answers as the expressions we began with, but we’re not going to make any mistakes in the formula due to copying or retyping.

3.2 Example: Japanese Flag

Let’s create another function. Remember our Japanese flag ([REF])? Each time we wanted a different-sized flag, we had to change the value of unit and re-run the whole program. Instead, we should create a function that generates Japanese flags.

How many parameters does this function need? Going back to our earlier code, we see that the only thing that really changes is unit. Everything else is calculated from that. Therefore, we should turn unit into a parameter, and keep the rest of the computation (which is already in terms of unit) intact:

fun japan-flag(unit): bg-width = unit * 3 bg-height = unit * 2 circ-rad = 3/5 * 1/2 * bg-height red-circ = circle(circ-rad, "solid", "red") white-rect = rectangle(bg-width, bg-height, "solid", "white") overlay(red-circ, white-rect) end

This function body creates several local [REF] variables, and eventually produces the result of the overlay expression, which is the flag shape. We can therefore use it many times:

japan-flag(100) japan-flag(200) japan-flag(50)

without having to re-run the program between changes.Note that if the generated image is large, Pyret will replace the acutal image with a thumbnail version of it. Click on the thumbnail to see the full image.

3.3 Tests: Keeping Track of 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 can be modified to read:

fun moon-weight(earth-weight): 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.

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, as a result of which it’s no longer so easy to tell that they behave the same way, and we will find that it can be difficult to make the body match the examples. In fact, this is such a common in real software production that professional programmers always write down such examples—called teststo make sure their programs are behaving as they expect.

3.4 Type Annotations

Suppose we were to call moon-weight on a string:

moon-weight("Armstrong")

Do Now!

What happens?

Pyret generates an error, saying that you can’t multiply a number by a string (whoever taught you arithmetic will be pleased to hear that).

In a function this small, it hardly matters. But if you had a much bigger function, it would be frustrating to get a similar error from deep in its bowels. Worse, if you get a function that someone else wrote, you need to read the entire function—which could be quite a bit larger—to figure out what kinds of values it consumes and produces.

Fortunately, we can do better. Pyret lets you write annotations on functions that indicate its values. Specifically, in the case of moon-weight, because it consumes and produces numbers, we would write:

fun moon-weight(earth-weight :: Number) -> Number: earth-weight * 1/6 end

We’ve left out the where examples for brevity, but you can write those too. Now, just by reading the function you can tell that it consumes a number (the :: Number part) and that it also produces one (the -> Number part).

Do Now!

What happens now when you run moon-weight("Armstrong")?

Do Now!

What would the annotations be on japan-flag?

Because japan-flag consumes a number and produces an image, we write:

fun japan-flag(unit :: Number) -> Image: bg-width = unit * 3 bg-height = unit * 2 circ-rad = 3/5 * 1/2 * bg-height red-circ = circle(circ-rad, "solid", "red") white-rect = rectangle(bg-width, bg-height, "solid", "white") overlay(red-circ, white-rect) end

Observe that these annotations are clearly optional: until this section, our functions had neither. In fact, you can use annotations in one place and not another. Also, you can place annotations on any new variable, not only those in parameters: for instance, the variables inside japan-flag can also be annotated.

Do Now!

Fill in the annotations in each of the blanks:

fun japan-flag(unit :: Number) -> Image: bg-width :: ___ = unit * 3 bg-height :: ___ = unit * 2 circ-rad :: ___ = 3/5 * 1/2 * bg-height red-circ :: ___ = circle(circ-rad, "solid", "red") white-rect :: ___ = rectangle(bg-width, bg-height, "solid", "white") overlay(red-circ, white-rect) end

The full-annotated function would be:

fun japan-flag(unit :: Number) -> Image: bg-width :: Number = unit * 3 bg-height :: Number = unit * 2 circ-rad :: Number = 3/5 * 1/2 * bg-height red-circ :: Image = circle(circ-rad, "solid", "red") white-rect :: Image = rectangle(bg-width, bg-height, "solid", "white") overlay(red-circ, white-rect) end

Do Now!

Change one of the annotations to be incorrect: e.g.,

red-circ :: Number = circle(circ-rad, "solid", "red")

  • When do you get an error? Is it when you click Run or only when you actually use japan-flag?

  • Which part of your program does the error refer to?

The things we put in the annotations—Number, String, etc.—are called types. Types help us tell apart different kinds of data. Every value has a type, and no value has more than one type. Thus, 3 is a Number (and no other type), "hello" is a String (and no other type), and so on.Later [REF] we will see that we can “refine” types so that a value can have more than one refined type: 3 can be a number, an odd number, but also a prime number, and so on. In some languages [REF], these type annotations are checked before the program runs, so you can learn about potential errors before ever running your program. In other languages, you only discover them during program execution. Pyret itself aims to provide both modes, so you can choose whichever makes most sense for your context.

3.5 Defining Functions in Steps

When writing functions, it is useful to write it in stages. First, give it a name, make sure you understand its types, and write a little documentation to remind your user and reader, who may be unfamiliar with your function—in a few weeks, this could be you!—what it’s meant to do. For instance, here’s a function that, given a number of hours worked, computes the corresponding salary:

fun hours-to-wages(hours :: Number) -> Number: doc: "Compute total wage from hours, with overtime, at $10/hr base" end

Note that the purpose statement above leaves implicit when “overtime” kicks in; in the USA, this is after 40 hours per week.

Next, we write down examples:

fun hours-to-wages(hours :: Number) -> Number: doc: "Compute total wage from hours, with overtime, at $10/hr base" where: hours-to-wages(40) is 400 hours-to-wages(40.5) is 407.5 hours-to-wages(41) is 415 hours-to-wages(0) is 0 hours-to-wages(45) is 475 hours-to-wages(20) is 200 end

Examples should cover at least all the different cases mentioned in the data definition. In this case, for example, it’s useful to note that the 40th hour doesn’t count towards overtime, but the 41st does. Note that by writing the examples the way we have above, it isn’t entirely clear what computation results in those answers; in contrast, writing

hours-to-wages(0) is 0 * 10 hours-to-wages(20) is 20 * 10 hours-to-wages(40) is 40 * 10

Note how we’ve written even the 0 as 0 * 10 to make clear we’re using a rate of $10 per hour…for zero hours.

Of course, we should also work out the calculation beyond 40 hours. Now the formula is going to get complicated. For the first 40 hours, the employee is paid their salary per hour, which contributes 40 * 10 (just as in the last example above). For every additional hour (i.e., the total hours worked with 40 subtracted), they are paid 1.5 times their salary, i.e., 10 * 1.5. Combining these two pieces, we get:

hours-to-wages(40.5) is (40 * 10) + ((40.5 - 40) * (10 * 1.5)) hours-to-wages(41) is (40 * 10) + ((41 - 40) * (10 * 1.5)) hours-to-wages(45) is (40 * 10) + ((45 - 40) * (10 * 1.5))

From these examples, we can determine the shape of the body:

fun hours-to-wages(hours :: Number) -> Number: doc: "Compute total wage from hours, with overtime, at $10/hr base" if hours <= 40: hours * 10 else: (40 * 10) + ((hours - 40) * (10 * 1.5)) end where: hours-to-wages(40) is 400 hours-to-wages(40.5) is 407.5 hours-to-wages(41) is 415 hours-to-wages(0) is 0 hours-to-wages(45) is 475 hours-to-wages(20) is 200 end

The hours-to-wages function always assumes an hourly rate of $10/hour. We can change it to accommodate a different hourly rate, say $20/hour, by changing the constant 10 where it appears representing the hourly rate:

fun hours-to-wages-20(hours :: Number) -> Number: doc: "Compute total wage from hours, accounting for overtime, at $20/hr base" if hours <= 40: hours * 20 else: (40 * 20) + ((hours - 40) * (20 * 1.5)) end end

We could make another copy of the function for $30/hour workers, and so on. However, it’s also possible, and quite straightforward, to change the function to work for any hourly wage. We note the shared parts across the implementation and lift them out, adding a new parameter to the function.

fun hours-to-wages-at-rate(rate :: Number, hours :: Number) -> Number: doc: "Compute total wage from hours, accounting for overtime, at the given rate" if hours <= 40: hours * rate else: (40 * rate) + ((hours - 40) * (rate * 1.5)) end end

Note that we’ll take the convention of adding new parameters at the beginning of the argument list. We simply add the new parameter (with an appropriate annotation), and replace all instances of the constant with it.

Exercise

Write a function called has-overtime that takes a number of hours and returns true if the number of hours is greater than 40 and false otherwise.

Exercise

Working negative hours is nonsense. Write a version of hours-to-wages that uses the raise function to throw an error if fewer than 0 hours are reported. Use the raises form to test for it (read about raises in the Pyret documentation).

Exercise

Write a function called hours-to-wages-ot that takes a number of hours, an hourly rate, and an overtime threshold, and produces the total pay. Any hours worked beyond the overtime threshold should be credited at 1.5 times the normal rate of pay, as before.