When writing complex programs, we often need to ask questions about the values with which we are computing. For example, should this entry come before this other entry when we sort the entries in a table or is this location within 100 miles of this second location? Frequently, these questions, which we often phrase as tests (not the same as unit tests), are used in control structures. For example, we might decide to do one thing if a value is a string and another if it is an integer.
To express these kinds of questions, we need a variety of tools. First, we need a type in which to express the valid answers to questions. Second, we need a collection of procedures that can answer simple questions. Third, we need ways to combine questions. Finally, we need control structures that use these questions. In the subsequent sections of this reading, we consider each of these issues. We will return to more complex control structures in another reading.
A Boolean value is a datum that reflects the outcome of a single
yes-or-no test. For instance, if one were to ask whether Des Moines is
within 100 miles of Boston, it would determine that the two cities are not
that close and it would signal this result by displaying the Boolean
value for “no” or “false”, which is #f. There is only one other Boolean
value, the one meaning “yes” or “true”, which is #t. These are called
“Boolean values” in honor of the logician George Boole who was the first
to develop a satisfactory formal theory of them. (Some folks now talk
about “fuzzy logic” that includes values other than “true” and “false”,
but that’s beyond the scope of this course.)
A predicate is a procedure that always returns a Boolean value. A
procedure call in which the procedure is a predicate performs some
yes-or-no test on its arguments. For instance, the predicate number?
(the question mark is part of the name of the procedure) takes one
argument and returns #t if that argument is a number, #f if it does
not. Similarly, the predicate even? takes one argument, which must be
an integer, and returns #t if the integer is even and #f if it is
odd. The names of most Scheme predicates end with question marks, and
Grinnell’s computer scientists recommend this useful convention, even
though it is not required by the rules of the programming language.
Scheme provides a wide variety of basic predicates and Scamper adds a few more. We will consider a few right now, but learn more as the course progresses.
The simplest predicates let you test the type of a value. Scheme provides a number of such predicates.
number? tests whether its argument is a number.integer? tests whether its argument is an integer.string? tests whether its argument is a string.procedure? tests whether its argument is a procedure.boolean? tests whether its argument is a Boolean value.list? tests whether its argument is a list.Scamper provides a predicate, equal? for testing whether two values
can be understood to be the same.
equal? tests whether its two arguments are the same or, in the case
of lists, whether they have the same contents.equal? works on any pair of values, even though they have different types. Of
course, we expect that if two values have different types, they should not be
equal. We commonly compare numbers for equality enough, that it is worthwhile
to have a version of equal? that only works on numbers.
= tests whether its arguments, which must all be numbers, are
numerically equal; 5 and 5.0 are numerically equal for this purpose.Scheme also provides many numeric predicates, some of which you may have already explored.
even? tests whether its argument, which must be an integer, is even.odd? tests whether its argument, which must be an integer, is odd.zero? tests whether its argument, which must be a number, is equal to zero.positive? tests whether its argument, which must be a real number, is positive.negative? tests whether its argument, which must be a real number, is negative.When we use a predicate to compare two values, most frequently to see if one should precede the other in some natural ordering, we often refer to that predicate as a “comparator”.
Scheme provides a number of numeric comparators.
< tests whether its arguments, which must all be numbers, are in
strictly ascending numerical order. (The < operation is one of the
few built-in predicates that does not have an accompanying question mark.)> tests whether its arguments, which must all be numbers, are in strictly
descending numerical order.<= tests whether its arguments, which must all be numbers, are in
ascending numerical order, allowing equality.>= tests whether its arguments, which must all be numbers, are in
descending numerical order, allowing equality.As you’ve studied other types, you may have seen other comparators. Here are some of the more common ones.
char<? tests whether itss arguments, which must all be characters,
are in strictly ascending alphabetical order.char<=? tests whether its arguments, which must all be characters,
are in ascending alphabetical order.char>? tests whether its arguments, which must all be characters,
are in strictly descending alphabetical order.char>=? tests whether its arguments, which must all be characters,
are in descending alphabetical order.char-ci<? tests whether itss arguments, which must all be characters,
are in strictly ascending alphabetical order, ignoring case.char-ci<=? tests whether its arguments, which must all be characters,
are in ascending alphabetical order, ignoring case.char-ci>? tests whether its arguments, which must all be characters,
are in strictly descending alphabetical order, ignoring case.char-ci>=? tests whether its arguments, which must all be characters,
are in descending alphabetical order, ignoring case.(char<? #\a #\a) (char<=? #\a #\a) (char<? #\a #\b) (char<? #\a #\b) (char-ci<? #\a #\b) (char<=? #\a #\a) (char-ci<=? #\a #\a)
string<? tests whether its arguments, which must all be strings,
are in strictly ascending alphabetical order.string<=? tests whether its arguments, which must all be strings,
are in ascending alphabetical order.string>? tests whether its arguments, which must all be strings,
are in strictly descending alphabetical order.string>=? tests whether its arguments, which must all be strings,
are in descending alphabetical order.string-ci<? tests whether its arguments, which must all be strings,
are in strictly ascending alphabetical order, but ignoring case.string-ci<=? tests whether its arguments, which must all be strings,
are in ascending alphabetical order, but ignoring case.string-ci>? tests whether its arguments, which must all be strings,
are in strictly descending alphabetical order, but ignoring case.string-ci>=? tests whether its arguments, which must all be strings,
are in descending alphabetical order, but ignoring case.notNot all the procedures we use to work with Boolean values are strictly
predicates. Another useful Boolean procedure is not, which takes
one argument and returns #t if the argument is #f and #f if the
argument is anything else. For example, one can test whether picture
is not an image with:
(import image) (define picture (square 100 "solid" "black")) (not (image? picture))
If Scheme says that the value of this expression is #t, then picture
is not an image.
and and orThe and and or keywords have simple logical meanings. In particular,
the and of a collection of Boolean values is true if all are true and
false if any value is false, the or of a collection of Boolean values
is true if any of the values is true and false if all the values are
false. For example,
(and #t #t #t) (and (< 1 2) (< 2 3)) (and (odd? 1) (odd? 3) (odd? 5) (odd? 6)) (and) (or (odd? 1) (odd? 3) (odd? 5) (odd? 6)) (or (even? 1) (even? 3) (even? 4) (even? 5)) (or)
You may note that we were careful to describe and and or as “keywords”
rather than as “procedures”. The distinction is an important one. Although
keywords look remarkably like procedures, Scheme distinguishes keywords
from procedures by the order of evaluation of the parameters. For
procedures, all the parameters are evaluated and then the procedure is
applied. For keywords, not all parameters need be evaluated, and custom
orders of evaluation are possible.
If and and or were procedures, we could not guarantee their control
behavior. We’d also get some ugly errors. For example, consider the
extended version of the even? predicate below:
(define new-even?
(lambda (val)
(and (integer? val) (even? val))))
Suppose new-even? is called with 2.3 as a parameter. In the keyword
implementation of and, the first test, (integer? ...),
fails, and new-even? returns false. If and were a procedure, we
would still evaluate the (even? ...), and that test would
generate an error, since even? can only be called on integers.
We can, of course, write our own predicates. For example, here is a predicate that determines whether its input, a real number, is between 0 and 100, inclusive.
(define valid-grade?
(lambda (val)
(<= 0 val 100)))
We can also write our own comparators. For example, here’s a somewhat pointless comparator that orders words based on their second letter.
;;; (second-letter<? str1 str2) -> boolean?
;;; str1 : string?
;;; str2 : string?
;;; Determine if the second letter of str1 alphabetically precedes
;;; the second letter of str2.
(define second-letter<?
(lambda (str1 str2)
(char-ci<? (string-ref str1 1)
(string-ref str2 1))))
and and orAs you may recall, it is useful to have a mental model for how things work in Scheme.
Our traditional model is that we evaluate the parameters to a procedure and then apply the procedure.
However, and and or behave a bit differently.
Here’s the basic behavior of and.
Note that we do not necessarily evaluate all of its parameters.
and has no parameters, replace (and) with #tand has exactly one parameter, evaluate that parameter and replace the and expression by the value of the parameter.and has more than one parameter, evaluate the first parameter.
#f).and and continue.Let’s look at a simple example.
(and (< 3 4) (= 4 4))
; Rule A3: More than one parameter
--> (and #t (= 4 4))
; Rule A3b: Parameter (#t) is true, so drop it.
--> (and (= 4 4))
; Rule A2: Exactly one parameter, evaluate it.
--> (and #t)
; Rule A2: Exactly one parameter, replace by the value of the parameter.
--> #t
Now, what about or? Here are the rules for or expressions.
or has no parameters, replace (or) with #for has exactly one parameter, evaluate that parameter and replace the and expression by the value of the parameter.or has more than one parameter, evaluate the first parameter.
Experience suggests that students understand and and or much better after a little general practice figuring out how they combine values. Fill in the following tables for each of the operations and and or. The third column of the table should be the value of (and arg1 arg2), where arg1 is the first argument and arg2 is the second argument. The fourth column should be the value of (or arg1 arg2).
arg1 |
arg2 |
(and arg1 arg2) |
(or arg1 arg2) |
|---|---|---|---|
#f |
#f |
||
#f |
#t |
||
#t |
#f |
||
#t |
#t |
We know how to compare strings alphabetically. Write a comparator,
(shorter? str1 str2) that returns true if the length of str1
is strictly less than the length of str2.
> (shorter? "a" "ab")
#t
> (shorter? "abc" "ab")
#f
> (shorter? "ab" "ba")
#f
> (shorter? "" "abc")
#t
This reading is closely based on a similar reading from CSC 151
2018S. We’ve removed a section on the comparator procedure, which we’ve
dropped from the class library, and added short sections on regular expressions,
filtering, and combining predicates. Much of the discussion of combining
predicates comes from a reading on filtering from CSC 151 2018S.
.
In Spring 2022, we added a section on tracing.