Lecture notes: Conditionals

Pen costs, revisited

Last time, we ended up with a pen-cost function that looked like this:

fun pen-cost(num-pens :: Number, message :: String) -> Number:
   num-pens * (0.25 + (string-length(message) * 0.02))
where:
  pen-cost(3, "wow") is 0.87
  pen-cost(10, "nice") is 10 * (0.25 + (string-length("nice") * 0.02))
end

We’re going to add one more thing to this function: a “docstring” describing what it does.

fun pen-cost(num-pens :: Number, message :: String) -> Number:
doc: "cost of ordering pens with slogans"
   num-pens * (0.25 + (string-length(message) * 0.02))
where:
  pen-cost(3, "wow") is 0.87
  pen-cost(10, "nice") is 10 * (0.25 + (string-length("nice") * 0.02))
end

Whenever you write a function, you should include a docstring.

Shipping costs

We’ve got a function now that computes the cost of pens, but our pen company has to account for another cost: shipping! It turns out that shipping costs $10 for orders of $50 or less, and $20 for orders of more than $50. So we’re going to write a function that takes the subtotal (i.e. the cost of the pens on their own) and adds on the shipping.

fun add-shipping(subtotal :: Number) -> Number:
  ...
end

Before we write the function, we can write some examples and a docstring.

fun add-shipping(subtotal :: Number) -> Number:
doc: "add shipping amount to subtotal"
  ...
where:
  add-shipping(1) is 1 + 10
  add-shipping(50) is 50 + 10
  add-shipping(50.01) is 50.01 + 20
  add-shipping(100) is 100 + 20
end

add-shipping adds a different amount depending on its input. In order to do that, we’ll need a feature we haven’t seen before: conditionals. We’ll use a conditional like this:

fun add-shipping(subtotal :: Number) -> Number:
doc: "add shipping amount to subtotal"
  if ...: # subtotal is $50 or less
    subtotal + 10
  else:
    subtotal + 20
  end
where:
  add-shipping(1) is 1 + 10
  add-shipping(50) is 50 + 10
  add-shipping(50.01) is 50.01 + 20
  add-shipping(100) is 100 + 20
end

Boolean expressions

What’s an expression that says “the subtotal is $50 or less?” Expressions that answer yes-or-no questions belong to a new type that we haven’t seen before: Boolean. There are two Boolean values: true and false. We can compare numbers and get Booleans:

> 30 <= 50
> 100 <= 50
> 100 > 50

Let’s say we have a number, subtotal. What are some questions we might ask about it?

> subtotal = 70
> 50 <= subtotal
> subtotal < 100
> (50 <= subtotal) and (subtotal < 100) # what happens if we leave off the parentheses?
> (subtotal <= 50) or (subtotal >= 70)

So in order to complete add-shipping, we can use these comparison operators.

fun add-shipping(subtotal :: Number) -> Number:
doc: "add shipping amount to subtotal"
  if subtotal <= 50:
    subtotal + 10
  else:
    subtotal + 20
  end
where:
  add-shipping(1) is 1 + 10
  add-shipping(50) is 50 + 10
  add-shipping(50.01) is 50.01 + 20
  add-shipping(100) is 100 + 20
end

Conditionals with multiple branches

What if shipping is more complicated? For instance, let’s say that shipping is still $20 orders between $50 and $100 (inclusive) but is $30 for orders over $100. We should first add some new tests:

fun add-shipping(subtotal :: Number) -> Number:
doc: "add shipping amount to subtotal"
  if subtotal <= 50:
    subtotal + 10
  else:
    subtotal + 20
  end
where:
  add-shipping(1) is 1 + 10
  add-shipping(50) is 50 + 10
  add-shipping(50.01) is 50.01 + 20
  add-shipping(100) is 100 + 20
  add-shipping(100.01) is 100.01 + 30
  add-shipping(200) is 200 + 30
end

How can we modify our function to do the right thing? A conditional expression can have any number of else if branches:

fun add-shipping(subtotal :: Number) -> Number:
doc: "add shipping amount to subtotal"
  if subtotal <= 50:
    subtotal + 10
  else if (subtotal > 50) and (subtotal <= 100):
    20
  else:
    30
  end
where:
  add-shipping(1) is 1 + 10
  add-shipping(50) is 50 + 10
  add-shipping(50.01) is 50.01 + 20
  add-shipping(100) is 100 + 20
  add-shipping(100.01) is 100.01 + 30
  add-shipping(200) is 200 + 30
end

Putting it all together

Now, how will we calculate the total cost of pens?

fun total-cost(num-pens :: Number, slogan :: String) -> Number:
doc: "compute the total cost of pens, including shipping"
  add-shipping(pen-cost(num-pens, slogan))
end

We’re using add-shipping as a helper function: a function that helps us break down a large problem into smaller subproblems. In this case, we have identified two subproblems: computing the base cost of pens, and adding in shipping costs.

More on helper functions

Let’s quickly revisit the snake patterns we talked about last time in order to find another example of helper functions. Last time we looked at code to create partial patterns; a function to create the full pattern is shown below:

fun stripe-pattern(color1 :: String, color2 :: String, color3 :: String)
  -> Image:
  frame(beside(rectangle(30, 100, "solid", color1),
      beside(rectangle(30, 100, "solid", color2),
        beside(rectangle(30, 100, "solid", color3),
          beside(rectangle(30, 100, "solid", color2),
            rectangle(30, 100, "solid", color1))))))
end

stripe-pattern("green", "black", "white")

Can we exploit the structure of this image to write something a little cleaner? Looking at the images, we see that there is an innermost rectangle, surrounded by two copies of another rectangle, then surrounded again by two copies of a third rectangle. If we had a surround operation, the function would be cleaner:

fun stripe-pattern(color1 :: String, color2 :: String, color3 :: String)
  -> Image:
  surround(rectangle(30, 100, "solid", color1),
    surround(rectangle(30, 100, "solid", color2"),
      rectangle(30, 100, "solid", color3)))
end

stripe-pattern("green", "black", "white")

Functions let us add operations like this. We can define surround:

fun surround(outer-image :: Image, inner-image :: Image)
  -> Image:
  beside(outer-image,
    beside(inner-image,
      outer-image))
end