3 From Repeated Expressions to Functions
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—
100 * 1/6 150 * 1/6 90 * 1/6
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.
fun moon-weight(earth-weight): earth-weight * 1/6 end
moon-weight(100) moon-weight(150) moon-weight(90)
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.
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
japan-flag(100) japan-flag(200) japan-flag(50)
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.
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
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—
3.4 Type Annotations
moon-weight("Armstrong")
Do Now!
What happens?
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—
fun moon-weight(earth-weight :: Number) -> Number: earth-weight * 1/6 end
Do Now!
What happens now when you run moon-weight("Armstrong")?
Do Now!
What would the annotations be on japan-flag?
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
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—
3.5 Defining Functions in Steps
fun hours-to-wages(hours :: Number) -> Number: doc: "Compute total wage from hours, with overtime, at $10/hr base" end
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
hours-to-wages(0) is 0 * 10 hours-to-wages(20) is 20 * 10 hours-to-wages(40) is 40 * 10
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))
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
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.