3.4 Conditionals and Booleans
3.4.1 Motivating Example: Shipping Costs
In Functions Practice: Cost of pens, we wrote a program (pen-cost
) to
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 add-shipping
computation.
Do Now!
Use the
is
notation fromwhere
blocks 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
.
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
Do Now!
What do you notice about our examples? What strategies do you observe across our choices?
Our proposed examples feature several strategic decisions:
Including
10
, which is at the boundary of charges based on the textIncluding
10.01
, which is just over the boundaryIncluding both natural and real (decimal) numbers
Including examples that should result in each shipping charge mentioned in the problem (
4
and8
)
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.
Do Now!
What is changing across our
add-shipping
examples? Do you notice anything different about these changes compared to the examples for our previous functions?
The values of
4
and8
differ across the examples, but they each occur in multiple examples.The values of
4
and8
appear 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.3.4.2 Conditionals: Computations with Decisions
To ask a question about our inputs, we use a new kind of expression
called an if expression. Here’s the full definition of add-shipping
:
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
In an if
expression, we ask a question that can produce an answer that
is true or false
(here 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 (order-amt +
8
). The 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.
3.4.3 Booleans
Every expression in Pyret evaluates in a value. So far, we have seen
three types of values: Number
, String
, and
Image
. What type of value does a question like order-amt
<= 10
produce? We can use the interactions prompt to experiment and
find out.
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
The values true
and false
belong to a new type in Pyret,
called Boolean
.Named for George Boole. While
there are an infinitely many values of type Number
, there are
only two of type Boolean
: true
and false
.
Exercise
What would happen if we entered
order-amt <= 10
at the interactions prompt to explore booleans? Why does that happen?
3.4.3.1 Other Boolean Operations
There are many other built-in operations that return Boolean
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
[Re-Examining Equality].
| |
|
| |
|
| |
|
| |
|
In general, ==
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.
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
"Ł"
is >
than "Z"
,
but in Polish, this should be false
. Worse, the ordering
depends on
location (e.g., Denmark/Norway vs. Finland/Sweden).Do Now!
Can you compare
true
andfalse
? Try comparing them for equality (==
), then for inequality (such as<
).
| |
|
| |
|
| |
|
| |
|
| |
|
Why use these operators instead of the more generic ==
?
Do Now!
Trynum-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.
|
| |
|
W
.
| |
|
3.4.3.2 Combining Booleans
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—
and
, or
, and not
. Here are some
examples of their use:
| |
|
| |
|
| |
|
| |
|
| |
|
Exercise
Explain why numbers and strings are not good ways to express the answer to a true/false question.
3.4.4 Asking Multiple Questions
add-shipping
program 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
8
from those in which the shipping charge is12
.The question for when the shipping charge is
8
will 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 order-amt
<= 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
where
examples that use the
12
charge.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 if
cases, thus accommodating
an arbitrary number of questions within a program.Do Now!
The problem description for
add-shipping
said that orders between10
and30
should incur an8
charge. 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.
Do Now!
How might you modify the above code to build the “between 10 and 30” requirement explicitly into the question for the
8
case?
and
operator on booleans? We can use that to
capture “between” relationships, as follows:
(order-amt > 10) and (order-amt <= 30)
Do Now!
Why are there parentheses around the two comparisons? If you replace
order-amt
with a concrete value (such as20
) 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 and
included:
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-shipping
support 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.
Exercise
An online-advertising firm needs to determine whether to show an ad for a skateboarding park to website users. Write a function
show-ad
that takes the age and haircolor of an individual user and returnstrue
if the user is between the ages of9
and18
and has either pink or purple hair.Try writing this two ways: once with
if
expressions 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.
3.4.5 Evaluating by Reducing Expressions
hours
contains the number of hours they work,
and suppose it’s 45
:
hours = 45
if hours <= 40:
hours * 10
else if hours > 40:
(40 * 10) + ((hours - 40) * 15)
end
hours
with 45
.
if 45 <= 40:
45 * 10
else if 45 > 40:
(40 * 10) + ((45 - 40) * 15)
end
if
expression is evaluated,
which in this case is false
.
=> 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
true
.
=> 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).
3.4.6 Composing Functions
pen-cost
for computing the cost of the pensadd-shipping
for 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
add-shipping
.
Do Now!
Write an expression that computes the total cost, with shipping, of an order of
10
pens that say"bravo"
.
There are two ways to structure this computation. We could pass the
result of pen-cost
directly to add-shipping
:
add-shipping(pen-cost(10, "bravo"))
Alternatively, you might have named the result of pen-cost
as
an intermediate step:
pens = pen-cost(10, "bravo")
add-shipping(pens)
Both methods would produce the same answer.
3.4.6.1 How Function Compositions Evaluate
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
pen-cost
.
Evaluating the second version: At a high level, Pyret goes through the following steps:
Substitute
10
fornum-pens
and"bravo"
formessage
in the body ofpen-cost
, then evaluate the substituted bodyStore
pens
in the directory, with a value of3.5
As a first step in evaluating
add-shipping(pens)
, look up the value ofpens
in the directorySubstitute
3.5
fororder-amt
in the body ofadd-shipping
then evaluate the resulting expression, which results in7.5
Evaluating the first version: As a reminder, the first version consisted of a single expression:
add-shipping(pen-cost(10, "bravo"))
Since arguments are evaluated before functions get called, start by evaluating
pen-cost(10, "bravo")
(again using substitution), which reduces to3.5
Substitute
3.5
fororder-amt
in the body ofadd-shipping
then evaluate the resulting expression, which results in7.5
Do Now!
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
explicitly named 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 pen-cost
)
gets substituted into the body of add-shipping
.
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.
3.4.6.2 Function Composition and the Directory
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 pen-cost
more
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
Do Now!
Write out the high level steps for how Pyret will evaluate the following program using this new version of
pen-cost
: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.
Do Now!
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 pens
and message-cost
will
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).
3.4.7 Nested Conditionals
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
tickets are $10
apiece.
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
15%
.
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
Boolean
to indicate whether the purchaser is a senior citizen.We have added an
if
expression 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
if
expression within the
else
case. This is valid code. (We could have also made an
else if
here, but we didn’t so that we could show that nested
conditionals are also valid).Exercise
Show the steps through which this function would evaluate in a situation where no discount applies, such as
buy-tickets3(2, false)
.
Do Now!
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 if
expression.
Do Now!
Under what conditions should the discount apply?
or
as 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
Do Now!
Take a look at the expression
is-senior == true
. What will this evaluate to when the value ofis-senior
istrue
? What will it evaluate to when the value ofis-senior
isfalse
?
== true
part is redundant. Since
is-senior
is 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
if
expression. 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
true
.Do Now!
What do you write to eliminate
== false
? For example, what might you write instead ofis-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
3.4.8 Recap: Booleans and Conditionals
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:
true
andfalse
.A simple kind of check (that produces a boolean) compares values for equality (
==
) or inequality(<>
). Other operations that you know from math, like<
and>=
, also produce booleans.We can build larger expressions that produce booleans from smaller ones using the operators
and
,or
,not
.We can use
if
expressions 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 totrue
orfalse
: you can just write the value or expression on its own (perhaps withnot
to get the same computation).