In Functions Practice: Cost of pens, we wrote a program (
compute the cost of ordering pens. Continuing the example, we now want
to account for shipping costs. We’ll determine shipping charges based
on the cost of the order.
Specifically, we will write a function
add-shipping to compute
the total cost of an order including shipping. Assume an order valued
at $10 or less ships for $4, while an order valued above $10 ships for
$8. As usual, we will start by writing examples of the
whereblocks to write several examples of add-shipping. How are you choosing which inputs to use in your examples? Are you picking random inputs? Being strategic in some way? If so, what’s your strategy?
Here is a proposed collection of examples for
add-shipping(10) is 10 + 4 add-shipping(3.95) is 3.95 + 4 add-shipping(20) is 20 + 8 add-shipping(10.01) is 10.01 + 8
What do you notice about our examples? What strategies do you observe across our choices?
Our proposed examples feature several strategic decisions:
10, which is at the boundary of charges based on the text
10.01, which is just over the boundary
Including both natural and real (decimal) numbers
Including examples that should result in each shipping charge mentioned in the problem (
So far, we have used a simple rule for creating a function body from examples: locate the parts that are changing, replace them with names, then make the names the parameters to the function.
What is changing across our
add-shippingexamples? Do you notice anything different about these changes compared to the examples for our previous functions?
The values of
8differ across the examples, but they each occur in multiple examples.
The values of
8appear only in the computed answers—
not as an input. Which one we use seems to depend on the input value.
add-shipping. In particular, we have clusters of examples that share a fixed value (the shipping charge), but different clusters (a) use different values and (b) have a pattern to their inputs (whether the input value is less than or equal to
10). This calls for being able to ask questions about inputs within our programs.
To ask a question about our inputs, we use a new kind of expression
called an if expression. Here’s the full definition of
fun add-shipping(order-amt :: Number) -> Number: doc: "add shipping costs to order total" if order-amt <= 10: order-amt + 4 else: order-amt + 8 end where: add-shipping(10) is 10 + 4 add-shipping(3.95) is 3.95 + 4 add-shipping(20) is 20 + 8 add-shipping(10.01) is 10.01 + 8 end
if expression, we ask a question that can produce an answer that
is true or false
order-amt <= 10, which we’ll explain below in
Booleans), provide one expression for
when the answer to the question is true (
order-amt + 4), and
another for when the result is false (
else in the program marks the answer in the false case; we call
this the else clause.
We also need
end to tell Pyret we’re done with the question and answers.
Every expression in Pyret evaluates in a value. So far, we have seen
three types of values:
Image. What type of value does a question like
<= 10 produce? We can use the interactions prompt to experiment and
Do Now!Enter each of the following expressions at the interactions prompt. What type of value did you get? Do the values fit the types we have seen so far?
3.95 <= 10 20 <= 10
false belong to a new type in Pyret,
Boolean.Named for George Boole. While
there are an infinitely many values of type
Number, there are
only two of type
What would happen if we entered
order-amt <= 10at the interactions prompt to explore booleans? Why does that happen?
There are many other built-in operations that return
values. Comparing values for equality is a common one: There is
much more we can and should say about equality, which we will do later
== checks whether two values are equal. Note this
is different from the single
= used to associate names with
values in the directory.
The last example is the most interesting: it illustrates that strings are case-sensitive, meaning individual letters must match in their case for strings to be considered equal.This will become relevant when we get to tables later.
"Z", but in Polish, this should be
false. Worse, the ordering depends on location (e.g., Denmark/Norway vs. Finland/Sweden).
Can you compare
false? Try comparing them for equality (
==), then for inequality (such as
Why use these operators instead of the more generic
num-equal("a", 1) string-equal("a", 1)
Therefore, it’s wise to use the type-specific operators where you’re expecting
the two arguments to be of the same type. Then, Pyret will signal an error if
you go wrong, instead of blindly returning an answer (
false) which lets
your program continue to compute a nonsensical value.
Often, we want to base decisions on more than one Boolean value. For
instance, you are allowed to vote if you’re a citizen of a country
and you are above a certain age. You’re allowed to board a bus
if you have a ticket or the bus is having a free-ride day. We
can even combine conditions: you’re allowed to drive if you are above
a certain age and have good eyesight and—
not. Here are some examples of their use:
Explain why numbers and strings are not good ways to express the answer to a true/false question.
add-shippingprogram to include a third shipping level: orders between $10 and $30 ship for $8, but orders over $30 ship for $12. This calls for two modifications to our program:
We have to be able to ask another question to distinguish situations in which the shipping charge is
8from those in which the shipping charge is
The question for when the shipping charge is
8will need to check whether the input is between two values.
The current body of
add-shipping asks one question:
order-amt <= 10. We need to add another one for
<= 30, using a charge of
12 if that question fails. Where do
we put that additional question?
An expanded version of the if-expression, using
else if, allows
you to ask multiple questions:
fun add-shipping(order-amt :: Number) -> Number: doc: "add shipping costs to order total" if order-amt <= 10: order-amt + 4 else if order-amt <= 30: order-amt + 8 else: order-amt + 12 end where: ... end
whereexamples that use the
if. It continues through the questions, returning the value of the answer of the first question that returns true. Here’s a summary of the if-expression syntax and how it evaluates.
if QUESTION1: <result in case first question true> else if QUESTION2: <result in case QUESTION1 false and QUESTION2 true> else: <result in case both QUESTIONs false> end
else ifcases, thus accommodating an arbitrary number of questions within a program.
The problem description for
add-shippingsaid that orders between
30should incur an
8charge. How does the above code capture “between”?
This is currently entirely implicit. It depends on us understanding the way an
if evaluates. The first question is
order-amt <= 10, so if we
continue to the second question, it means
order-amt > 10. In this
context, the second question asks whether
order-amt <= 30. That’s how
we’re capturing “between”-ness.
How might you modify the above code to build the “between 10 and 30” requirement explicitly into the question for the
andoperator on booleans? We can use that to capture “between” relationships, as follows:
(order-amt > 10) and (order-amt <= 30)
Why are there parentheses around the two comparisons? If you replace
order-amtwith a concrete value (such as
20) and leave off the parenthesis, what happens when you evaluate this expression in the interactions pane?
Here is what
add-shipping look like with the
fun add-shipping(order-amt :: Number) -> Number: doc: "add shipping costs to order total" if order-amt <= 10: order-amt + 4 else if (order-amt > 10) and (order-amt <= 30): order-amt + 8 else: order-amt + 12 end where: add-shipping(10) is 10 + 4 add-shipping(3.95) is 3.95 + 4 add-shipping(20) is 20 + 8 add-shipping(10.01) is 10.01 + 8 add-shipping(30) is 30 + 8 add-shipping(32) is 32 + 12 end
add-shippingsupport the same examples. Are both correct? Yes. And while the first part of the second question (
order-amt > 10) is redundant, it can be helpful to include such conditions for three reasons:
They signal to future readers (including ourselves!) the condition covering a case.
They ensure that if we make a mistake in writing an earlier question, we won’t silently get surprising output.
They guard against future modifications, where someone might modify an earlier question without realizing the impact it’s having on a later one.
An online-advertising firm needs to determine whether to show an ad for a skateboarding park to website users. Write a function
show-adthat takes the age and haircolor of an individual user and returns
trueif the user is between the ages of
18and has either pink or purple hair.
Try writing this two ways: once with
ifexpressions and once using just boolean operations.
Responsible Computing: Harms from Reducing People to Simple Data
Assumptions about users get encoded in even the simplest functions. The advertising exercise shows an example in which a decision gets made on the basis of two pieces of information about a person: age and haircolor. While some people might stereotypically associate skateborders with being young and having colored hair, many skateborders do not fit these criteria and many people who fit these criteria don’t skateboard.
While real programs to match ads to users are more sophisticated than this simple function, even the most sophisticated advertising programs boil down to tracking features or information about individuals and comparing it to information about the content of ads. A real ad system would differ in tracking dozens (or more) of features and using more advanced programming ideas than simple conditionals to determine the suitability of an ad (we’ll discuss some of these later in the book). This example also extends to situations far more serious than ads: who gets hired, granted a bank loan, or sent to or released from jail are other examples of real systems that depend on comparing data about individuals with criteria maintained by a program.
From a social responsibility perspective, the questions here are what data about individuals should be used to represent them for processing by programs and what stereotypes might those data encode. In some cases, individuals can be represented by data without harm (a university housing office, for examples, stores student ID numbers and which room a student is living in). But in other cases, data about individuals get interpreted in order to predict something about them. Decisions based on those predictions can be inaccurate and hence harmful.
hourscontains the number of hours they work, and suppose it’s
hours = 45
if hours <= 40: hours * 10 else if hours > 40: (40 * 10) + ((hours - 40) * 15) end
if 45 <= 40: 45 * 10 else if 45 > 40: (40 * 10) + ((45 - 40) * 15) end
ifexpression is evaluated, which in this case is
=> if false: 45 * 10 else if 45 > 40: (40 * 10) + ((45 - 40) * 15) end
false, the next branch is tried.
=> if 45 > 40: (40 * 10) + ((45 - 40) * 15) end
=> if true: (40 * 10) + ((45 - 40) * 15) end
true, the expression reduces to the body of that branch. After that, it’s just arithmetic.
=> (40 * 10) + ((45 - 40) * 15)
=> 400 + (5 * 15) => 475
This style of reduction is the best way to think about the evaluation of Pyret expressions. The whole expression takes steps that simplify it, proceeding by simple rules. You can use this style yourself if you want to try and work through the evaluation of a Pyret program by hand (or in your head).
pen-costfor computing the cost of the pens
add-shippingfor adding shipping costs to a total amount
What if we now wanted to compute the price of an order of pens
including shipping? We would have to use both of these functions
together, sending the output of
pen-cost to the input of
Write an expression that computes the total cost, with shipping, of an order of
10pens that say
There are two ways to structure this computation. We could pass the
pen-cost directly to
Alternatively, you might have named the result of
an intermediate step:
pens = pen-cost(10, "bravo") add-shipping(pens)
Both methods would produce the same answer.
Let’s review how these programs evaluate in the context of
substitution and the directory. We’ll start with the second
version, in which we explicitly name the result of calling
Evaluating the second version: At a high level, Pyret goes through the following steps:
messagein the body of
pen-cost, then evaluate the substituted body
pensin the directory, with a value of
As a first step in evaluating
add-shipping(pens), look up the value of
pensin the directory
order-amtin the body of
add-shippingthen evaluate the resulting expression, which results in
Evaluating the first version: As a reminder, the first version consisted of a single expression:
Since arguments are evaluated before functions get called, start by evaluating
pen-cost(10, "bravo")(again using substitution), which reduces to
order-amtin the body of
add-shippingthen evaluate the resulting expression, which results in
Contrast these two summaries. Where do they differ? What about the code led to those differences?
The difference lies in the use of the directory: the version that
pens uses the directory. The other version
doesn’t use the directory at all. Yet both approaches lead to the same
result, since the same value (the result of calling
gets substituted into the body of
This analysis might suggest that the version that uses the directory is somehow wasteful: it seems to take more steps just to end up at the same result. Yet one might argue that the version that uses the directory is easier to read (different readers will have different opinions on this, and that’s fine). So which should we use?
Use whichever makes more sense to you on a given problem. There will be times when we prefer each of these styles. Furthermore, it will turn out (once we’ve learned more about nuances of how programs evaluate) that the two versions aren’t as different as they appear right now.
Let’s try one more variation on this problem. Perhaps seeing us name
the intermediate result of
pen-cost made you wish that we had
used intermediate names to make the body of
readable. For example, we could have written it as:
fun pen-cost(num-pens :: Number, message :: String) -> Number: doc: ```total cost for pens, each 25 cents plus 2 cents per message character``` message-cost = (string-length(message) * 0.02) num-pens * (0.25 + message-cost) where: ... end
Write out the high level steps for how Pyret will evaluate the following program using this new version of
pens = pen-cost(10, "bravo") add-shipping(pens)
Hopefully, you made two entries into the directory, one for
message-cost inside the body of
pen-cost and one for
pens as we did earlier.
Consider the following program. What result do you think Pyret should produce?
pens = pen-cost(10, "bravo") cheap-message = (message-cost > 0.5) add-shipping(pens)
Using the directory you envisioned for the previous activity, what answer do you think you will get?
Something odd is happening here. The new program tries to use
message-cost to define
cheap-message. But the name
message-cost doesn’t appear anywhere in the program, unless we
peek inside the function bodies. But letting code peek inside function
bodies doesn’t make sense: you might not be able to see inside the
functions (if they are defined in libraries, for example), so this
program should report an error that
message-cost is undefined.
Okay, so that’s what should happen. But our discussion of the
directory suggests that both
be in the directory, meaning Pyret would be able to use
message-cost. What’s going on?
This example prompts us to explain one more nuance about the
directory. Precisely to avoid problems like the one illustrated here
(which should produce an error), directory entries made within a
function are local (private) to the function body. When you call a function,
Pyret sets up a local directory that other functions can’t
see. A function body can add or refer to names in either its local,
private directory (as with
message-cost) or the overall
(global) directory (as with
pens). But in no case can one
function call peek inside the local directory for another function
call. Once a function call completes, its local directory disappears
(because nothing else would be able to use it anyway).
We showed that the results in
if-expressions are themselves
expressions (such as
order-amt + 4 in the following function):
fun add-shipping(order-amt :: Number) -> Number: doc: "add shipping costs to order total" if order-amt <= 10: order-amt + 4 else: order-amt + 8 end end
The result expressions can be more complicated. In fact, they could be
entire if-expressions!. To see an example of this, let’s develop
another function. This time, we want a function that will compute the
cost of movie tickets. Let’s start with a simple version in which
fun buy-tickets1(count :: Number) -> Number: doc: "Compute the price of tickets at $10 each" count * 10 where: buy-tickets1(0) is 0 buy-tickets1(2) is 2 * 10 buy-tickets1(6) is 6 * 10 end
Now, let’s augment the function with an extra parameter to indicate
whether the purchaser is a senior citizen who is entitled to a discount. In such cases, we will reduce the overall price by
fun buy-tickets2(count :: Number, is-senior :: Boolean) -> Number: doc: ```Compute the price of tickets at $10 each with senior discount of 15%``` if is-senior == true: count * 10 * 0.85 else: count * 10 end where: buy-tickets2(0, false) is 0 buy-tickets2(0, true) is 0 buy-tickets2(2, false) is 2 * 10 buy-tickets2(2, true) is 2 * 10 * 0.85 buy-tickets2(6, false) is 6 * 10 buy-tickets2(6, true) is 6 * 10 * 0.85 end
The function now has an additional parameter of type
Booleanto indicate whether the purchaser is a senior citizen.
We have added an
ifexpression to check whether to apply the discount.
We have more examples, because we have to vary both the number of tickets and whether a discount applies.
Now, let’s extend the program once more, this time also offering the discount if the purchaser is not a senior but has bought more than 5 tickets. Where should we modify the code to do this? One option is to first check whether the senior discount applies. If not, we check whether the number of tickets qualifies for a discount:
fun buy-tickets3(count :: Number, is-senior :: Boolean) -> Number: doc: ```Compute the price of tickets at $10 each with discount of 15% for more than 5 tickets or being a senior``` if is-senior == true: count * 10 * 0.85 else: if count > 5: count * 10 * 0.85 else: count * 10 end end where: buy-tickets3(0, false) is 0 buy-tickets3(0, true) is 0 buy-tickets3(2, false) is 2 * 10 buy-tickets3(2, true) is 2 * 10 * 0.85 buy-tickets3(6, false) is 6 * 10 * 0.85 buy-tickets3(6, true) is 6 * 10 * 0.85 end
ifexpression within the
elsecase. This is valid code. (We could have also made an
else ifhere, but we didn’t so that we could show that nested conditionals are also valid).
Show the steps through which this function would evaluate in a situation where no discount applies, such as
Look at the current code: do you see a repeated computation that we might end up having to modify later?
Part of good code style is making sure that our programs would be easy
to maintain later. If the theater changes its discount policy, for
example, the current code would require us to change the discount
0.85) in two places. It would be much better to have that
computation written only one time. We can achieve that by asking which
conditions lead to the discount applying, and writing them as the
check within just one
Under what conditions should the discount apply?
oras follows (we’ve left out the examples because they haven’t changed from the previous version):
fun buy-tickets4(count :: Number, is-senior :: Boolean) -> Number: doc: ```Compute the price of tickets at $10 each with discount of 15% for more than 5 tickets or being a senior``` if (is-senior == true) or (count > 5): count * 10 * 0.85 else: count * 10 end end
Take a look at the expression
is-senior == true. What will this evaluate to when the value of
true? What will it evaluate to when the value of
== truepart is redundant. Since
is-senioris already a boolean, we can check its value without using the
==operator. Here’s the revised code:
fun buy-tickets5(count :: Number, is-senior :: Boolean) -> Number: doc: ```Compute the price of tickets at $10 each with discount of 15% for more than 5 tickets or being a senior``` if is-senior or (count > 5): count * 10 * 0.85 else: count * 10 end end
ifexpression. As a general rule, your code should never include
== true. You can always take that out and just use the expression you were comparing to
What do you write to eliminate
== false? For example, what might you write instead of
is-senior == false?
Finally, notice that we still have one repeated computation: the base
cost of the tickets (
count * 10): if the ticket price changes,
it would be better to have only one place to update that price. We can
clean that up by first computing the base price, then applying the
discount when appropriate:
fun buy-tickets6(count :: Number, is-senior :: Boolean) -> Number: doc: ```Compute the price of tickets at $10 each with discount of 15% for more than 5 tickets or being a senior``` base = count * 10 if is-senior or (count > 5): base * 0.85 else: base end end
With this chapter, our computations can produce different results in different situations. We ask questions using if-expressions, in which each question or check uses an operator that produces a boolean.
There are two Boolean values:
A simple kind of check (that produces a boolean) compares values for equality (
==) or inequality(
<>). Other operations that you know from math, like
>=, also produce booleans.
We can build larger expressions that produce booleans from smaller ones using the operators
We can use
ifexpressions to ask true/false questions within a computation, producing different results in each case.
We can nest conditionals inside one another if needed.
You never need to use
==to compare a value to
false: you can just write the value or expression on its own (perhaps with
notto get the same computation).