Skip to main content

Chapter 5 Making Decisions

Section 5.1 Boolean Values

So far, we’ve only seen Racket code that uses numbers. Numbers are cool, but there’s other kinds of data out there, and we should broaden our horizons a bit. In programming a good next step is to look at boolean values, which is a fancy name for values that are either true or false. With boolean values, we can add logic to our functions, eventually writing functions that behave differently depending on the parameters. Whevener we work with a function that retusn a boolean value, we can imagine it asking a question, like Is this number odd?, Are these values equal?...
In Racket, there are different ways of representing literal boolean values. We can either use true and false or #t and #f. There is no major advantage to one set over another, though there’s a resonable readability argument to go with true false.

Section 5.2 Comparison Functions

Comparison functions take numbers as arguments and return boolean values based on the type of comparison being performed.
  • =: Equals. Ex. (= a b)
  • >: Greater than. Ex. (> a b)
  • <: Less than. Ex. (< a b)
  • >=: Greater than or equal. Ex. (>= a b)
  • <=: Less than or equal. Ex. (<= a b)
Like the arithmetic functions, these functions can all take two or more arguments. (= a b c) would return true only if all three values are equal. (< a b c) would return true only if a is less than b and b is less than c.

Section 5.3 Boolean Functions

Boolean functions return boolean values but they also take in boolean values as arguments. These are also called logical operators because they directly correspond to logical operations that you may have learned about in a math class at some point. In Racket, we have three boolean functions:
  • (and p q)
    • Returns true only when all the arguments are also true
    • Returns false when any of the arguments are false
  • (or p q)
    • Returns true when any of the arguments are true
    • Returns false only when all the arguments are also true
  • (not p)
    • Returns the opposite boolean value of p
Like the comparison functions, and and or can both take two or more arguments. not only works with a single argument.

Section 5.4 What if?...

So now that we’ve got a whole lot of boolean tools to work with, what can we do with them? One thing is we can use a boolean value to change how a function behaves.
Let’s say you were writing a program that works with computer color values. Traditionally those values cannot be greater than 255. Suppose you want to write a function that would make sure you have a valid color value. If the input is less than or equal to 255, then the function should return the original value. But if the input is greater than 255, then the function should return 255 (since that is the largest possible value). So here we have a function that returns its parameter sometimes, and other times 255.
In Racket, there is a function called if that does exactly what we want!. if is a function that takes three arguments. The first is a boolean value and the next two are any kind of value. When the boolean parameter is true, the first parameter is returned, otherwise, the second is. Generally, if works like this:
(if BOOLEAN
  RESULT_IF_TRUE
  RESULT_IF_FALSE
)
Using if, we can now write our color validation function:
(define validColor
  (lambda (c)
    (if (>= c 255)
      255
      c)
  ))
The if function in validColor returns either c or 255, making it fairly straightforward. But remember that in Racket, we can always use a function that returns a value in place of a value (when necessary). Consider a slightly more complex task, a function that takes in a valid color value (so you can assume it is less than 255) changes it to be closer to the middle instead of the extremes:
(define mutedColor
  (lambda (c)
    (if (< c 128)
      (+ 25 c)
      (- 25 c)
  )))

Section 5.5 Nesting

Sometimes, a single true/false test does not cover all possible cases. Take our computer color problem from before. Colors must be less than or equal to 255, but they also have to be greater than or equal to 0, a case that was conveniently omitted before. A more useful validColor function would be able to work as already defined, but also return 0 if a negative color value was provided.
  • (validColor 176) returns 176
  • (validColor 987) returns 255
  • (validColor -32) returns 0
The problem is that if can only give one of two possible results, but we need three possible results. In addition to what we’ve already coded, we need to check if the provided color is less than 0.
(if (>= c 0)
  c
  0)
So how do we incorporate this function into our exiting validColor code? Well, remember that if is a function that returns a value, and we can always use a function in place of a value:
(define validColor
  (lambda (c)
    (if (>= c 255)
      255
      (if (>= c 0)
        c
        0))
  ))
We’ve taken the false result of our original function, which was c and replaced it with a section if function, which will return c or 0. Generally speaking, when we place an if function inside another if function, we call this nesting. There’s no (reasonable) limit to how deep your nesting can go, it all depends on the problem you’re trying to solve. There’s also no reason that you can double-nest, that is use an if function for both the true and false sections of another if.

Section 5.6 In Too Deep

At some point, you may find that nesting if functions gets cumbersome. At about the third or fourth level in, you’ve tabbed pretty far over, and your code can get less readable. Consider the following if function:
(if (<=  s 14)
    14
    (if (<= s 18)
      18
      (if (<= s 23)
        23
        (if (<= s 28)
          28
          (if (<= s 34)
            34
            42)))))
This is a perfectly valid if function, but some would argue that because there are some many nested functions, it looses out on the all-important readability scale. In Racket, there is another decision-making function designed specifically when you have a series of related conditions to check.
cond is a function that has the ability to produce different results based on multiple boolean tests:
(cond
  (TEST0 RESULT0)
  (TEST1 RESULT1)
  (TEST2 RESULT2)
  ...
  (else DEFAULT)
)
Every TEST statement above must be a boolean value (or a function that returns a boolean value). The RESULTs or DEFAULT can be any value. cond will return the RESULT attached the the first TEST that is/returns true, all subsequent functions within cond will be skipped.
You’ll notice the last test/result pair has the word else, which has a special meaning for use with cond. In the event that all the boolean test of a cond statement are false, the result attached else will be returned. This makes sure that cond will always return a value, regardless of your tests, which is generally a good thing.
If we take our super-nested if function, and translate into cond, it would look like this:
(cond
  ((<=  s 14) 14)
  ((<=  s 18) 18)
  ((<=  s 23) 23)
  ((<=  s 28) 28)
  ((<=  s 34) 34)
  (else 42)
)
It is worth noting that cond is not necessary, you can get the same results using if. The reason cond exists is to make certain functions easier to understand/write. If you prefer if you can just stick with it. Conversely, you could use cond in place of all your if functions, if you really dig how cond works. That being said, whether or not you have a preference, you should understand how each works.