Working with Functions
1 Cleaning Up Flag Code
We started again with the function from last class to make three-stripe flags.
fun three-stripe-flag(top :: String, middle :: String, bot :: String): |
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 |
|
armenia = three-stripe-flag("red", "blue", "orange") |
austria = three-stripe-flag("red", "white", "red") |
We observed that someone might want to make flags of different sizes, but that flags also have a height/width ratio that should be preserved. This suggests taking one dimension (we chose width) as an input and computing the other:
fun three-stripe-flag(top :: String, middle :: String, bot :: String, |
width :: Number): |
doc: "produce image of flag with three equal height horizontal stripes" |
frame( |
above(rectangle(width, width * 0.25, "solid", top), |
above(rectangle(width, width * 0.25, "solid", middle), |
rectangle(width, width * 0.25, "solid", bot)))) |
end |
|
armenia = three-stripe-flag("red", "blue", "orange", 120) |
austria = three-stripe-flag("red", "white", "red", 120) |
We talked about how this code uses the same ratio three times, so it should really be a constant:
RATIO = 0.25 |
|
fun three-stripe-flag(top :: String, middle :: String, bot :: String, |
width :: Number): |
doc: "produce image of flag with three equal height horizontal stripes" |
frame( |
above(rectangle(width, width * RATIO, "solid", top), |
above(rectangle(width, width * RATIO, "solid", middle), |
rectangle(width, width * RATIO, "solid", bot)))) |
end |
|
armenia = three-stripe-flag("red", "blue", "orange", 120) |
austria = three-stripe-flag("red", "white", "red", 120) |
Then we talked about two other modifications: we might want to do the width * RATIO computation only once, storing the result in a variable:
RATIO = 0.25 |
|
fun three-stripe-flag(top :: String, middle :: String, bot :: String, |
width :: Number): |
doc: "produce image of flag with three equal height horizontal |
stripes" |
height = width * RATIO |
|
frame( |
above(rectangle(width, height, "solid", top), |
above(rectangle(width, height, "solid", middle), |
rectangle(width, height, "solid", bot)))) |
end |
Alternatively, we could create a helper function to do the height computation:
RATIO = 0.25 |
|
fun compute-height(width :: Number): |
doc: "Compute height from width based on ratio |
width * RATIO |
end |
|
fun three-stripe-flag(top :: String, middle :: String, bot :: String, |
width :: Number): |
doc: "produce image of flag with three equal height horizontal |
stripes" |
frame( |
above(rectangle(width, compute-height(width), "solid", top), |
above(rectangle(width, compute-height(width), "solid", middle), |
rectangle(width, compute-height(width), "solid", bot)))) |
end |
The last version illustrates that you can create additional functions as a way to name repeated intermediate computations. These functions are typically called helper functions. In this specific case, just naming the height computation is sufficient. Sometimes, however, a helper function is useful for letting you name more complex intermediate computations.
2 More Functions Practice: Cost of pens
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 (count spaces as characters for now).
First, let’s write two expressions that do this computation, just to make sure we understand how it should work:
# if ordering 3 pens that say "wow" |
3 * (0.25 + (string-length("wow") * 0.02)) |
|
# if ordering 10 pens that say "smile" |
10 * (0.25 + (string-length("smile") * 0.02)) |
Having a clear idea of the computation you want to do makes it much easier to write a function to do it. How do we figure out a function that would correspond to these two expressions? We find the places where the two functions are different and replace each spot of difference with a unique descriptive name. These names become the parameters of the function.
fun pen-cost(num-pens :: Number, message :: String): |
num-pens * (0.25 + (string-length(message) * 0.02)) |
The initial expressions we developed give rise to our examples. When you write the expected results of the examples, you can either write the final answer or the expression that computed it (each can be useful in certain cases). Here’s the final function:
fun pen-cost(num-pens :: Number, message :: String): |
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 0.87 |
pen-cost(10, "smile") is 10 * (0.25 + (string-length("smile") * 0.02)) |
What are other good cases to check here? Ordering 0 pens and pens with empty messages are both good cases to check. You don’t have to check negative numbers of pens or messages that aren’t strings though – those fall outside the expected "contract" of the function (we’ll discuss how to properly handle the negatives later).
Once you are comfortable with the idea of writing functions, you would skip this circle-and-label step, and just jump directly to writing the expression with the parameters in place. Use the circle-and-label technique as needed to get yourself to that point.
3 Conditionals: Peppers
The Scoville-organoleptic test is used to determine how hot a pepper is by determining the ratio of pepper extract to sugar water that is needed to remove the heat of the pepper when tasted. We want to write a program that produces the name of a pepper based on its Scoville rating. Here are the rating ranges we’ll use for this problem:
bell peppers have rating 0
paprika has ratings 100 - 1000
jalapeno has ratings 3,500 - 10,000
habenero has ratings 100,000 - 350,000
If there is no name for a given rating number, then the program will return the string "unknown".
Let’s write some examples of the function in a where block:
fun pepper-scale(rating :: Number) -> String: |
doc: "given a scoville rating, return the name of the pepper" |
where: |
pepper-scale(0) is "bell pepper" |
pepper-scale(250) is "paprika" |
pepper-scale(60000000) is "unknown" |
end |
In this case, the output strings depend on some attribute of the input (in this case, what range they lie in). So to write this code, we need to be able to ask a question about the input in order to determine the output. This requires a construct called if/else expressions. Here’s the code. The textbook explains these expressions and how they evaluate:
fun pepper-scale(rating :: Number) -> String: |
doc: "given a scoville rating, return the name of the pepper" |
if (rating == 0): |
"bell pepper" |
else if ((rating >= 100) and (rating <= 1000)): |
"paprika" |
else if ((rating >= 3500) and (rating <= 10000)): |
"jalapeno" |
else if ((rating >= 100000) and (rating <= 350000)): |
"habanero" |
else: |
"unknown" |
end |
where: |
pepper-scale(0) is "bell pepper" |
pepper-scale(250) is "paprika" |
pepper-scale(60000000) is "unknown" |
end |
4 Design Challenge: Flags and Discounts
Finally, we started to pull together everything we covered this week by starting work on the following problem:
The flag store needs to compute the cost of a flag order, which differs based on the size of the flag. Small flags are $10 and large flags are $20, but a customer gets 20% discount if they order more than 15 flags
How might you approach this problem? What constants would you define? What functions would you create?
We’ll return to this problem in the next lecture.