Deciding with if and cond
Two truth values, the rule that only one of them is false, and the two ways to choose between branches: if and cond.
Truth, and the one thing that is false
Scheme has #t for true and #f for false, and a rule worth remembering: everything except #f counts as true. Zero is true. An empty list is true. A string is true. This is the opposite of many languages, where zero and empty values are treated as false.
The tests you would expect are there: = for numbers, >, <, >= and <=, and equal? for strings and lists. Each one answers with #t or #f, which is exactly what the decision forms below are waiting for.
if takes two roads, cond takes many
(if test yes no) picks one of two expressions. The test is worked out first, and only the chosen branch is evaluated — that last part matters, because it means a branch can safely do something that only makes sense when the test passed.
cond is the chain of tests you reach for once two branches are not enough. Each line is a test and its answer, and the word else catches everything left over. Read it downwards: the first test that comes out true wins, and the rest are never looked at.
A test is just an expression
The test in an if can be any expression, not only a comparison. (if (null? xs) ...) and (if (member x xs) ...) both work, because every value except #f counts as true.
That rule makes a bare name a useful test. If name holds a string, (if name ...) takes the yes branch; if it holds #f, it takes the no branch. The cost is that values you might expect to be false, such as zero or the empty list, are true, so ask the question you actually mean.
cond clauses work the same way. Each clause is a test and a body, and the first test that is not #f wins. A clause may hold several expressions, and the last one is that clause’s answer.
Short-circuit, and why it matters
and and or stop as soon as the answer is settled. (and #f (car (list))) never calls car, because the first value already decided the result; (or #t (car (list))) never calls it either.
That is what makes a guard safe. (and (pair? x) (> (car x) 0)) checks that x is a pair before asking for its first value, so the second test cannot fail on an empty list.
The value handed back is the deciding one, not a fresh #t. A test that only needs yes or no can ignore the difference; a program that stores the result should know it.
Choosing, and combining tests
(define (grade n)
(cond ((>= n 90) "A")
((>= n 80) "B")
(else "keep going")))
(print (grade 95))
(print (grade 82))
(print (grade 40))
(print (if #t "this one" "not this one"))
(print (and (> 3 2) (< 1 2)))
(print (or #f 0 "second"))
(print (not #f))Only one branch runs
if evaluates its test, then exactly one of the two branches. The other is not evaluated at all, which is why a branch can safely divide by a value that might be zero, as long as the test has already ruled that case out. A program that seems to fail inside a branch it did not take is usually failing in the test.
cond has the same rule, clause by clause. The first test that is not #f wins, its body runs, and the remaining clauses are never touched. That is why the order is part of the meaning, and why a broad test placed too high makes the narrow ones unreachable.
The value of a decision is a value
Both forms answer a value: the last expression of the branch that ran. That is why a decision can sit on the right of a definition, or inside a string-append, without a temporary name. A branch that only displays answers something unspecified, so use it for its effect rather than its value, and put the value you want in the last position.
Three traps. if takes exactly two branches after the test, so a third expression is an error; use cond or when for more. A cond with no else answers nothing useful when every test fails, which is easy to miss in the output. And = compares numbers only, so text needs equal? or string=?.
Try these next
- Rewrite the
sizechain with the largest case first and explain what changes. - Print
(and 1 2 3)and(or #f #f)and read the values, not just true or false. - Write a
condthat labels a number as negative, zero or positive.
Three sizes and a chain of tests
Run it and follow the three calls to `size` down the cond. Then add a size of your own for numbers below zero, and change the `when` test so the last line does not print.
Ticket prices, with a guard
Run it and match each price to the clause that produced it. Then add a category for people over 65, and make members of any age pay two less.
and and or are not just yes-and-no words: they hand back the deciding value. (or #f 0 "second") answers 0, because 0 is the first value that is not #f, and any value that is not #f counts as true. (and 1 2) answers 2, because 1 passed and 2 was the last value.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
Which of these values counts as false in a test?
