Conditions#
if
syntax#
Conditions allow to take decisions while executing a program, like “if temperature is higher than 20 °C, display that the weather is sunny”. The syntax is:
(if condition expression-if-yes expression-if-no)
Unlike many languages where if
is an instruction which executes code if the
condition is met, Scheme’s if
is an expression. The essential difference is
that an expression evaluates to a value, while an instruction performs an action
without returning a value. Again, you have to reframe your mind to think a
little differently: instead of “if the temperature is higher than 20°C, assign
sunny to weather, else, assign gloomy to weather”, you will typically
say this in Scheme: “let weather be the value that is sunny if the
temperature is higher than 20°C and gloomy otherwise”. Here is a first
example:
(define (equal-or-different x y)
(display
(if (= x y)
"x and y are equal."
"x and y are different."))
(newline))
(equal-or-different 4 4)
⊨ x and y are equal.
(equal-or-different 4 5)
⊨ x and y are different.
As you likely guessed, the =
procedure tests whether two numbers are equal.
What kind of value does it return?
(= 4 5)
⇒ #f
This test was evaluated to #f
, which is the Scheme notation for the boolean
“false”. The opposite of #f
is #t
(“true”).
(= 5 5)
⇒ #t
Importantly, only one of the two expressions is evaluated, depending on the value of the test. The previous example could also have been written like this:
(if (= x y)
(display "x and y are equal.")
(display "x and y are different."))
If \(x \neq y\), the expression (display "x and y are equal.")
is never
evaluated, which is fortunate, since otherwise both messages would be printed
on the console. Another advantage is that this allows programs such as:
(define (display-inverse x)
(if (= x 0)
(display "can't divide by zero")
(display (/ 1 x)))
(newline))
(display-inverse 0)
⊨ can't divide by zero
This function prints the inverse of its argument \(x\), i.e. \(\frac1x\). This
inverse only exists if \(x \neq 0\), as you can’t divide by 0. Had if
evaluated
both of its arguments, the expression (/ 1 x)
would have been evaluated even
if \(x = 0\), causing an error.
cond
syntax#
cond
is an equivalent to what is written in other languages as
if ... else if ... else if ...
. This syntax is a practical way to write code
that needs to branch between many possible cases. Imagine that you are coding
the part of LilyPond that prints a note head depending on its style. You could
write something like this:
(if (style-is-default)
(print-default-note-head)
;; Else ...
(if (style-is-altdefault)
(print-altdefault-note-head)
;; Else ...
(if (style-is-baroque)
(print-baroque-note-head)
;; Else ...
(if (style-is-mensural)
(print-mensural-note-head)
;; Else ...
(if (style-is-petrucci)
(print-petrucci-note-head)
;; Else ...
etc. etc. etc.)))))
Obviously, this is tedious and there are excessively many levels of nesting.
This is where cond
comes in handy. Its syntax is:
(cond
(condition1 expression1)
(condition2 expression2)
(condition3 expression3)
...)
The conditions are evaluated in order. As soon as one is true, the corresponding
expression is evaluated, and the entire cond
expression takes its value.
In the place of the last condition, you can write else
. This is the same as
writing #t
. The last expression is then always evaluated in case no other
condition was true.
Here is an example using cond
:
(define (category temperature)
(cond
((> temperature 50)
"unbearable temperature")
((> temperature 40)
"hardly bearable temperature")
((> temperature 30)
"hot to very hot temperature")
((> temperature 20)
"moderate temperature")
((> temperature 10)
"rather cold temperature")
((> temperature 0)
"cold temperature")
((> temperature -10)
"very cold temperature")
(else
"polar temperature")))
(display (category 28))
⊨ moderate temperature
Frequently used tests#
The first test you met was =
. It applies exclusively to numbers. It tests
their numeric equality, i.e., it does not take into account whether they are
integer or floating-point numbers.
(= 3 3.0)
⇒ #t
Another frequently used equality test is equal?
. It applies to all data types,
not just numbers. For example, you can use it to compare strings.
(equal? "Hello" "Hello")
⇒ #t
Even on numbers, =
and equal?
are not equivalent, since equal?
does not
consider numbers of different types as equal.
(equal? 3 3.0)
⇒ #f
The question mark is part of the name of equal?
. Syntactically, it is no
problem in Scheme to have a question mark in the name of a variable (see
Defining variables). By convention, a question mark is used at the end of the name of
any procedure that performs a test, returning true or false. Such procedures are
called “predicates”.
To compare numbers, there are procedures <
, <=
, >
and >=
, which are read
“less than”, “less than or equal”, “greater than” and “greater than or equal”.
They are equivalent to the mathematical symbols \(<\), \(\leq\), \(>\) and \(\geq\).
(>= 4 3)
⇒ #t
(>= 3 4)
⇒ #f
(> 3 3)
⇒ #f
(>= 3 3)
⇒ #t
Combining tests#
There are two main logical operators to combine tests together:
and
checks whether all conditions are true;or
checks whether at least one of the conditions is true.
(define (GPS location)
(if (or (equal? location "North Pole")
(equal? location "South Pole"))
(display "Put on your coat!")
(display "Not at the pole yet."))
(newline))
(GPS "North Pole")
⊨ Put on your coat!
(GPS "Helsinki")
⊨ Not at the pole yet.
(define (detect-requiem composer work)
(cond
((and (equal? work "Requiem")
(equal? composer "Mozart"))
(display "Mozart's Requiem"))
((equal? work "Requiem")
(display "A Requiem (not Mozart's)"))
(else
(display "Not a Requiem"))))
(detect-requiem "Mozart" "Requiem")
⊨ Mozart's Requiem
The not
operator inverts the result of a test.
(define (conversation is-musician preferred-software)
(cond
((not is-musician)
(display "What a pity for you!"))
((not (equal? preferred-software "LilyPond"))
(display "Do you know a fantastic piece of software called LilyPond?"))
(else
(display "How about starting to contribute?")))
(newline))
(conversation #t "LilyPond")
⊨ How about starting to contribute?
One-armed if
#
Quite often, there is nothing to do if a condition is not met. This is
particularly true when what is done if the condition is true is printing an
error message. For these cases, there is a second form of if
, where the
expression to be evaluated if the condition is false is simply omitted.
(if condition expression-if-yes)
For example:
(define (check-composer name)
(if (equal? name "unknown")
(error "composer not found in the database")))
(check-composer "unknown")
If the condition is true, expression-if-yes
is evaluated, and the if
takes
its value, as in a usual if
. If, on the other hand, the condition is false,
the expression has no interesting value. Actually, it prints nothing in the
sandbox:
guile> (if #t "value if yes")
"value if yes"
guile> (if #f "value if yes")
guile>
Still, the expression does have a value!
guile> *unspecified*
guile> (equal? *unspecified* (if #f "value if yes"))
#t
This value is *unspecified*
, a special constant returned by all functions that
do not need to return any special value. The display
function also returns
*unspecified*
. In order not to clutter the interactive session, the sandbox
simply ignores *unspecified*
.
On universal truth#
if
expressions do not accept only booleans as conditions. In many languages
other than Scheme, certain types have their own rules regarding truthiness.
Typically, numbers would be true except if 0, and strings or lists would be true
except if empty.
Scheme is quite different. Absolutely all values are true, except the boolean
#f
. In particular, 0 and empty strings are true.
(if 0 "yes" "no")
⇒ "yes"
This is why the value #f
is usually used to represent missing values, unfound
values, etc. With this convention, the check-composer
can be rewritten as:
(define (check-composer name) ; name is a string or #f
(if (not name) ; detects #f
(error "composer not found in database")))
Just like if
, and
and or
are lazy: they only evaluate their arguments when
it turns out necessary. Also, they do not necessarily return a boolean. When all
of its arguments are true, and
returns the value of the last argument. When
one of its arguments is true, or
returns the first such argument. This
principle is what gives rise to a common trick for defining default values.
(define (display-composer composer)
(display
(or composer "[unknown]"))
(newline))
(display-composer "Mozart")
⊨ Mozart
(display-composer #f)
⊨ [unknown]
Let us analyze what happens step-by-step. If composer
is a true value (namely
anything but #f
), the test (or composer "[unknown"])
can stop executing, as
or
has already found a true value (just one is needed). It is then this
value that or
returns. On the other hand, if composer
is #f
, the or
test cannot stop immediately and evaluates the next expression, "[unknown]"
.
This expression is true, so or
stops and returns it.