On this page:
3.4.1 Motivating Example:   Shipping Costs
3.4.2 Conditionals:   Computations with Decisions
3.4.3 Booleans
3.4.3.1 Other Boolean Operations
3.4.3.2 Combining Booleans
3.4.4 Asking Multiple Questions
3.4.5 Evaluating by Reducing Expressions
3.4.6 Composing Functions
3.4.6.1 How Function Compositions Evaluate
3.4.6.2 Function Composition and the Directory
3.4.7 Nested Conditionals
3.4.8 Recap:   Booleans and Conditionals

3.4 Conditionals and Booleans

    3.4.1 Motivating Example: Shipping Costs

    3.4.2 Conditionals: Computations with Decisions

    3.4.3 Booleans

      3.4.3.1 Other Boolean Operations

      3.4.3.2 Combining Booleans

    3.4.4 Asking Multiple Questions

    3.4.5 Evaluating by Reducing Expressions

    3.4.6 Composing Functions

      3.4.6.1 How Function Compositions Evaluate

      3.4.6.2 Function Composition and the Directory

    3.4.7 Nested Conditionals

    3.4.8 Recap: Booleans and Conditionals

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 from where 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:

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?

Two things are new in this set of examples:
  • The values of 4 and 8 differ across the examples, but they each occur in multiple examples.

  • The values of 4 and 8 appear only in the computed answers—not as an input. Which one we use seems to depend on the input value.

These two observations suggest that something new is going on with 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].

1 == 1

true

1 == 2

false

"cat" == "dog"

false

"cat" == "CAT"

false

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.

Sometimes, we also want to compare strings to determine their alphabetical order. Here are several examples:

"a" < "b"

true

"a" >= "c"

false

"that" < "this"

true

"alpha" < "beta"

true

which is the alphabetical order we’re used to; but others need some explaining:

"a" >= "C"

true

"a" >= "A"

true

These use a convention laid down a long time ago in a system called ASCII.Things get far more complicated with non-ASCII letters: e.g., Pyret thinks "Ł" 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 and false? Try comparing them for equality (==), then for inequality (such as <).

In general, you can compare any two values for equality (well, almost, we’ll come back to this later); for instance:

"a" == 1

false

If you want to compare values of a specific kind, you can use more specific operators:

num-equal(1, 1)

true

num-equal(1, 2)

false

string-equal("a", "a")

true

string-equal("a", "b")

false

Why use these operators instead of the more generic ==?

Do Now!

Try

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.

There are even more Boolean-producing operators, such as:

wm = "will.i.am"

string-contains(wm, "will")

true

Note the capital W.

string-contains(wm, "Will")

false

In fact, just about every kind of data will have some Boolean-valued operators to enable comparisons.

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 andeither pass a test or have a temporary license. Also, you’re allowed to drive if you are not inebriated.

Corresponding to these forms of combinations, Pyret offers three main operations: and, or, and not. Here are some examples of their use:

(1 < 2) and (2 < 3)

true

(1 < 2) and (3 < 2)

false

(1 < 2) or (2 < 3)

true

(3 < 2) or (1 < 2)

true

not(1 < 2)

false

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

Shipping costs are rising, so we want to modify the 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 is 12.

  • The question for when the shipping charge is 8 will need to check whether the input is between two values.

We’ll handle these in order.

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

At this point, you should also add where examples that use the 12 charge.

How does Pyret determine which answer to return? It evaluates each question expression in order, starting from the one that follows 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

A program can have multiple else if cases, thus accommodating an arbitrary number of questions within a program.

Do Now!

The problem description for add-shipping said that orders between 10 and 30 should incur an 8 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?

Remember the 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 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 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

Both versions of 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:
  1. They signal to future readers (including ourselves!) the condition covering a case.

  2. They ensure that if we make a mistake in writing an earlier question, we won’t silently get surprising output.

  3. 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 returns true if the user is between the ages of 9 and 18 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

In How Functions Evaluate, we talked about how Pyret reduces expressions and function calls to values. Let’s revisit this process, this time expanding to consider if-expressions. Suppose we want to compute the wages of a worker. The worker is paid $10 for every hour up to the first 40 hours, and is paid $15 for every extra hour. Let’s say hours contains the number of hours they work, and suppose it’s 45:

hours = 45

Suppose the formula for computing the wage is

if hours <= 40:
  hours * 10
else if hours > 40:
  (40 * 10) + ((hours - 40) * 15)
end

Let’s now see how this results in an answer, using a step-by-step process that should match what you’ve seen in algebra classes (the steps are described in the margin notes to the right): The first step is to substitute the hours with 45.

if 45 <= 40:
  45 * 10
else if 45 > 40:
  (40 * 10) + ((45 - 40) * 15)
end

Next, the conditional part of the if expression is evaluated, which in this case is false.

=>  if false:
      45 * 10
    else if 45 > 40:
      (40 * 10) + ((45 - 40) * 15)
    end

Since the condition is false, the next branch is tried.

=>  if 45 > 40:
      (40 * 10) + ((45 - 40) * 15)
    end

Pyret evaluates the question in the conditional, which in this case produces true.

=>  if true:
      (40 * 10) + ((45 - 40) * 15)
    end

Since the condition is 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

We started this chapter wanting to account for shipping costs on an order of pens. So far, we have written two functions:
  • pen-cost for computing the cost of the pens

  • add-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:

Evaluating the first version: As a reminder, the first version consisted of a single expression:

add-shipping(pen-cost(10, "bravo"))

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

There are a couple of things to notice here:
  • 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

Notice here that we have put a second 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?

Here, we see that the discount applies if either the purchaser is a senior or more than 5 tickets have been bought. We can therefore simplify the code by using 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

This code is much tighter, and all of the cases where the discount applies are described together in one place. There are still two small changes we want to make to really clean this up though.

Do Now!

Take a look at the expression is-senior == true. What will this evaluate to when the value of is-senior is true? What will it evaluate to when the value of is-senior is false?

Notice that the == 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

Notice the revised question in the 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 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

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.