6 Conditionals and Booleans
6.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.6.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.
6.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?
6.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
.
| |
|
6.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.
6.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 window?
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 + 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.
6.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).
6.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.
Exercise
Manually evaluate each version. Where are the sequences of evaluation steps the same and where do they differ across these two programs?
6.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
6.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).