Conditions

if syntax

Conditions allow to take decisions while executing a program, like “if temperature is higher than 30 °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 et y are equal."
        "x et y are different."))
  (newline))

(equal-or-different 4 4)
 x et y are equal.
(equal-or-different 4 5)
 x et 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 expresson2)
  (condition3 expresson3)
  ...)

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")
    ((> 30 temperature)
     "hot to very hot temperature")
    ((> 20 temperature)
     "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.

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 is 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.