Programming Languages and Paradigms

We are discussing the idea of *syntactic abstractions*,
those features of a language that are convenient to have but
that are not absolutely essential to the language. At this
point, we have considered how the following common features of
a programming language can, in fact, be considered abstractions
of other, more primitive features: functions that take more than
one argument, local variables, and local functions. This section
considers logical connectives, conditional expressions, and case
analysis.

Like most languages, Racket provides ways to write compound Boolean expressions. For example:

> (and (> 1 0) (< 1 0)) #f > (and (procedure? car) (procedure? cons)) #t > (or (> 1 0) (< 1 0)) #t > (or (procedure? 'car) (procedure? 'cons)) #f > (not (< 1 0)) #t > (not (procedure? cons)) #f

Logical operators such as `and` and `or` have a
special semantics. We have already seen that having variable
arity is nothing special, but there is something else. To see
what is special about them, let's consider a function definition
of `and` using our evaluation model. When we evaluate
a function, we (1) evaluate each of the subexpressions and (2)
apply the leftmost result to the rest.

If `and` were a function, then it would have to evaluate
all of its arguments. But consider the expression
`(and #f #t #t #t #t #t #t #t)`. Before the evaluator
applied `and`, it would have to evaluate seven expressions
to `#t` in order to determine that the value of expression
is `#f`. Even if each of these subexpressions required an
expensive function call, itwould have to evaluate all of them
*first*.

For this reason, most languages offer logical connectives that do "short-circuit evaluation" of their arguments. As soon as the interpreter encounters a value that determines the value of the whole expression, it returns that value. This means that, in function, the connectives must be special forms, not functions.

We can describe how short-circuit evaluation works with the
inductive definitions for `and` and `or`. These
definitions show how to translate one expression into another,
with a simpler version of the operator in question.

This `and` expression:

(andis equal totest_1test_2...test_n)

(iftest_1(andtest_2...test_n) ;; looking for a reason false) ;; to return false...

And this `or` expression:

(oris equal totest_1test_2...test_n)

(let ((*value*test_1)) (if *value* ;; looking for a reason *value* ;; to return true... (ortest_2...test_n)))

Why does the translation of the `or` expression use a
`let` expression? Because the terms passed to an
`or` expression may not be variable references. They can
be expressions of any sort. If the term is an expensive function
call:

(or (find 'a massive-tree) (find 'b another-massive-tree) ...)

... you don't want to compute `(find 'a massive-tree)`
twice: once to see that it is not false and once to return it.

(if (find 'a massive-tree) (find 'a massive-tree) (if (find 'b another-massive-tree) (find 'b another-massive-tree) ...)))

If the term is not purely functional and has side effects, then such re-evaluation may even give the wrong answer!

These translations show us that `and` and `or` are
really syntactic abstractions of the more general conditional
expression `if`. Indeed, many of you still avoid use of
`and` and `or` by building `if` expressions
with which you feel more comfortable. But this can be dangerous,
because you have to handle all of the alternative cases properly.
The structure of an `if` or a `cond` is considerably
more complex. The connectives may be sugar, but their sweetness
is worth a little effort.

What if `n` is 0 in the above expressions? What should
`and` and `or` return then? We can take a hint
from the patterns established here. `and` looks for
"false" conjuncts. If it finds one, then it returns false;
otherwise, it continues to look. When there are no conjuncts,
it will not find a false one, *so and returns true*.
(Think of this from the perspective of a recursive function...)

Likewise, `or` looks for "true" disjuncts. If it finds
one, then it returns true; otherwise, it continues to look.
When there are no disjuncts, it will not find a true one, so
`or` returns false.

We have had a glimpse of Racket's multi-part branching construct,
`cond`, in a
previous lecture.
Here is an example of the use of a `cond`:

> (define sign (lambda (x) (cond ((< x 0) -1) ((> x 0) 1) (else 0)) )) > (sign -2) -1 > (sign 4) 1 > (sign 0) 0

We can provide an inductive semantic translation of `cond`
to `if` as follows:

(cond (<p1> <e1>) (<p2> <e2>) . . . (<pn> <en>))is equal to

(if <p1> <e1> (cond (<p2> <e2>) . . . (<pn> <en>)) )

This definition is not complete, because it doesn't take into
account the optional `else` expression. Still, this gives
us a good idea of what is going on.

Quick Exercise: How would you modify the definition to takeelseinto account?

The biggest difference between `if` and `cond` is
that `cond` allows more than two choices. Any expression
that can be written using one can be rewritten using the other.

Quick Exercise, in Reverse: Give an inductive translation of anifexpressions into acondexpression.

The fact that all `if`s can be written as equivalent
`cond`s means that neither expression is more fundamental
than the other. An interpreter that can handle one can also
handle the other, as well as logical connectives, as syntactic
abstractions.

Racket provides a `case` special form that is similar to
the `switch` statements of C/C++ and Java. (Check it out in
the Racket Guide.)
If you would like to practice more with the idea of conditional
operators as syntactic abstractions, use the translations given
in this reading to determine the main differences between a
`case` expression and a more general conditional construct.
You might even try writing an inductive definition for it!