Scheme Programming/Numbers and Expressions

In this section, we'll continue to work with numbers, since they're familiar and easy to reason about. We'll also introduce the Scheme type system and consider more carefully how Scheme expressions are evaluated.

Types
First, though, what do we mean when we say a Scheme object is a "number" or a "boolean"? These names denote the type of an object. Very generally, knowing an object's type gives us information about what we can do with it. If we know that $$a$$ and $$b$$ both have type boolean, we know that the AND operation of logic is defined on $$a$$ and $$b$$; that is, the value of ($$a$$ AND $$b$$) will be meaningful. We say that such a meaningful expression is well-typed. If, on the other hand, $$b$$ has type number, we have no way to evaluate ($$a$$ AND $$b$$), because we don't know how to make sense of AND unless both of its arguments are booleans. In this case, the expression is ill-typed and its value is undefined.

Scheme is a strongly-typed language, which means that ill-typed expressions are forbidden; evaluating one will cause Scheme to report an error. For example, we can expect Scheme to reject the following expression:

From the error message, we can infer that the Scheme procedure expects its arguments to be numbers. Since is a boolean, the expression  is ill-typed; no meaningful value can be obtained, so the execution of the program halts. In this way, Scheme prevents further surprises which we might encounter were execution to continue with a bogus value.

How do we get type information about Scheme objects? Scheme types are defined by type predicates. These are procedures which, given any Scheme object as an argument, return if the object has a specific type, and  otherwise. By applying a type predicate  for a type foo, then, we can divide the set of all Scheme objects into objects of type foo and the rest.

So far, we've worked with numbers and booleans, which have the type predicates  and, as you might have guessed, .

Scheme guarantees that the basic types, including numbers and booleans, are disjoint. This means, for example, that no Scheme object is a number and a boolean; it might be one or the other or neither, but it cannot be both. In type-predicate terms, there's no Scheme object  such that   and are both.

We won't use basic type predicates like  too often in this book, but it's important to understand how they define the Scheme type system. They are used constantly in core Scheme and library procedures, for example, to ensure that arguments are well-typed.

Numbers
We looked at several examples of simple numerical programs in the previous section. So far, though, the programs we've seen have only used integers. Scheme has a rich system of number types, called the numerical tower, which lets us compute with rational, real, and complex numbers.

As these examples show, Scheme also gives us many ways to write numbers. Rational numbers are written in the form , reals can be written in exponential ("scientific") notation as  (which gives the value $$m \times 10^n$$), and complex numbers are written in the rectangular form   (or ), where a is the real part and b the imaginary part.

As we see in the final, nested example, Scheme allows us to mix different kinds of numbers without having to manually convert them to a common form.

Scheme also provides a wealth of numerical procedures. Here are a few examples:

See R7RS § 6.2.6 for a comprehensive list of numerical procedures.

Expressions
By this point, we've seen Scheme evaluate a number of expressions (and, hopefully, you've tried them out in your Scheme interpreter). However, we've been vague about the meaning of Scheme expressions. It's easy to guess that  is evaluated by adding the numbers 2 and 3, but guessing isn't enough when we're faced with complex nesting or expressions containing things less familiar than, say, natural numbers or addition. We need a model of evaluation for the Scheme expressions we've been seeing. What follows is a simplified version, but one that is correct for the programs we'll look at in this chapter.

The expression  may seem a bit trivial to evaluate, but let's step through it. This expression is an application of the operator  to the operands and. To evaluate an application,


 * First, evaluate the operator and operands.


 * Then, apply the procedure which is the value of the operator to the values of the operands.

We saw in the very first example in "A taste of Scheme" that asking Scheme to evaluate a number gives us the number back as the value. This observation gives us the general rule of evaluation for numbers:


 * Evaluating numbers: Numbers are self-evaluating.

With this rule in hand, it's trivial to evaluate the operands. Now we need the value of, the operator. This value is a procedure object. This value is opaque; that is, it's a "black box" for performing addition that Scheme gives us, and we can't see its inner workings. We can then apply this value to the values of the operands to obtain 5, the value of the whole expression.

A more interesting example is the expression . Here, the operator is  and the operands are   and . To find the value of this application, we must evaluate these subexpressions, which are themselves applications! Here is one possible sequence of evaluation steps:

(* (+ 2 3) (- 7 5)) = (* 5 (- 7 5))  = (* 5 2)  = 10

More complicated expressions are evaluated by exactly the same process; see exercise 1 below. So long as we are dealing with expressions built entirely out of applications, then, we know how to evaluate them; we recursively apply our evaluation rules until the expression is fully evaluated.