In a computer program described by sequences of expressions, it is often the case that a decision has to be made between two possible expressions. In such cases, a conditional expression selects which of these two expressions evaluates next.
This chapter introduces conditional expressions and shows if
expressions, logical operators, predicates, when
, unless
, and cond
expressions.
Suppose you have a test to select between two choices. If the test is true, then one choice is made, othewise, the alternative is made. In Scheme, such a method of decision making is called an if
expression.
To write an if
expression, you start with parentheses, where the first element is the if
identifier, the second element is the test, the third element is the consequent, and the final fourth element is the alternative. In other words, an if
expression is in the form of: (if <test> <consequent> <alternative>)
.
But before I describe evaluation of if
, recall that false is #f
and true is #t
and both of these are self-evaluating expressions:
> #t
$1 = #t
> #f
$2 = #f
An if
expression is evaluated as follows. First, the <test>
is evaluated. If it yields a true value, then <consequent>
is evaluated and its value is returned. Otherwise, <alternative>
is evaluated and its value is returned.
> (if #t 1 2)
$3 = 1
> (if #f 1 2)
$4 = 2
An if
expression evaluates either the <consequent>
or the <alternative>
, but never both. This is very important. In Scheme, an if
expression evaluates in normal order, unlike other symbolic expressions, which evaluate in applicative order.
Note that the <alternative>
is optional. When <test>
evaluates to #f
and the <alternative>
is omitted, the expression result is undefined.
The if
primitive expression is the fundamental conditional in Scheme, out of which all other conditionals, such as cond
, case
, when
, or lambda-case
, are built upon.
Often if
or any of its above-mentioned derivations are used in combination with logical operators and predicates, both of which are described next.
There are at least three different operations that can be applied to #f
and #t
values. The first operation is the procedure not
, and the other two operations are the macros and
and or
.
> not
$1 = #<procedure not (_)>
Let us implement my-not
by writing a procedure that accepts one argument, denoted by z
:
(define (my-not z)
An if
expression is used to test whether the argument z
is true and it then returns false:
(define (my-not z)
(if z #f #t))
> (my-not 1)
$1 = #f
> (my-not #t)
$2 = #f
> (my-not #f)
$3 = #t
It is revealing to see that #f
and #t
can be implemented as procedures. This is not how these are implemented in Guile. Rather the purpose here is to show that you can implement not
, true
, and false
without using any data structures at all but only using procedures.
Let us start with the indentity procedure, which accepts x
and returns x
:
(lambda (x) x)
Suppose you add one more argument, denoted by y
. The return value can now be either ‘x’ or ‘y’:
(lambda (x y) x)
(lambda (x y) y)
Consider what happens when a not
expression is called with an argument z
. If z
is true, then return false, otherwise return true. This is similar to the above-shown identity procedures with two arguments.
Suppose the following definitions:
> (define my-true (lambda (x y) x))
> (define my-false (lambda (x y) y))
Now simply call these functions with themselves as arguments:
> (my-true my-false my-true)
$4 = #<procedure my-false (x y)>
> (my-false my-false my-true)
$5 = #procedure my-true (x y)>
Clearly, the not operation is:
> (define (my-not z) (z my-false my-true))
> (my-not my-false)
$6 = #<procedure my-true (x y)>
> (my-not my-true)
$7 = #<procedure my-false (x y)>
Unlike not
which is a procedure, the and
and or
operations are implemented as macros. To understand why, consider the following example:
(and #t #f <exp-4>)
In Scheme, the arguments of and
expressions are evaluated left-to-right, until a false value is reached or no more expressions are found. In the example above, <exp-4>
is never evaluated because the expression to the left evaluates into #f
. This is why and
is a macro, and not a procedure, which would evaluate <exp-4>
regardless of the expressions to the left.
So, the following two expressions do not result in an error
> (and #t #f (4 does not error as it never evaluates))
$8 = #f
> (or #f #t (4 does not error as it never evaluates))
$9 = #t
To this end, you are not ready to write my-and
and my-or
as macros, because we did not learn recursion. I leave this for a later chapter.
Recall that the if
expression must contain a test, whose outcome determines the program flow. The Scheme standards define various tests for expressions. We call such tests predicates. Here are eight examples:
> (procedure? +)
#1 = #t
> (procedure? (lambda (x) x))
$2 = #t
> (number? 37)
$3 = #t
> ((lambda (x) (and (number? x) (even? x))) 50)
$4 = #t
> ((lambda (x) (and (number? x) (odd? x))) 50)
#5 = #f
> (zero? 37)
#6 = #f
> (and (boolean? #f) (boolean? #t))
$7 = #t
> (boolean? 73)
$8 = #f
A predicate is a procedure that returns true or false. They conventionally have names ending with a question mark. Predicates are often used in combination with if
expressions:
> ((lambda (x) (if (even? x) (/ x 2) x)) 73)
$9 = 73
> ((lambda (x) (if (even? x) (/ x 2) x)) 48)
$10 = 24
Let us now see expressions derived using if
expressions.
Recall that expressions have a result and a side effect. Sometimes, of interest is the side effect but not the result of an if
expression. In Scheme, you clearly express this idea by using a when
expression, which is simply an if
expression without an <alternative>
. Similarly, unless
is if
without an <alternative>
, but with a negated <test>
.
> ((lambda (x) (when (even? x) x)) 48)
$1 = 48
> ((lambda (x) (unless (even? x) x)) 73)
$2 = 73
Both when
and unless
are macros which cannot be implemented as procedures. The reason why when
and unless
cannot be procedures is that the <consequent>
expression is not always evaluated, depending on the result of the test. But a procedure always evaluates all arguments, meaning that a procedure implementation of when
and unless
would always evaluate the <consequent>
, regardless of the test, which is not the desired behaviour.
Let us write my-when
as a macro. As always, begin with:
(define-syntax my-when (lambda (x) (syntax-case x ()
The expression before expansion (EBE) is simply the form of the when
expression:
(define-syntax my-when (lambda (x) (syntax-case x ()
((my-when test exp-1 exp* ...)
The expression after expansion (EAE) is:
(define-syntax my-when (lambda (x) (syntax-case x ()
((my-when test exp-1 exp* ...)
#'(if test (begin exp-1 exp* ...))))))
Let us write my-unless
as a macro. As always, begin with:
(define-syntax my-unless (lambda (x) (syntax-case x ()
The EBE is simply the form of the unless
expression:
(define-syntax my-unless (lambda (x) (syntax-case x ()
((my-unless test exp-1 exp* ...)
The EAE is simply the form of the when expression:
(define-syntax my-unless (lambda (x) (syntax-case x ()
((my-unless test exp-1 exp* ...)
#'(if (not test) (begin exp-1 exp* ...))))))
Let us expand my-when
and my-unless
:
> ,expand (my-when (even? x) x)
$3 = (if (even? x) x)
> ,expand (my-unless (even? x) x)
$4 = (if (not (even? x)) x)
Sometimes many if
expressions are chained together. Starting from the parent if
the child if
is the <alternative>
expression.
Here is an example of a level-three chain of if
expressions
(if <test-1>
<consequent-1>
(if <test-2>
<consequent-2>
(if <test-3>
<consequent-3>
<alternative>)))
The Scheme standards define the cond
macro (derived expression). Here is how to rewrite the previous three-level if-chain into a cond
derived expression
(cond
(<test-1> <consequent-1>)
(<test-2> <consequent-2>)
(<test-3> <consequent-3>)
(else <alternative>))
To this end, you are not ready to write my-cond
as a macro, because you did not learn recursion nor let
expressions.