Cool Tricks with Conditions

The fundamental design of Lisp lets you get a lot of mileage out of a few simple commands. Specifically, a couple of counterintuitive tricks involving conditions in Lisp can help you write cleaner code. The first involves two new conditional commands. The second takes advantage of Lisp’s simple conception of true and false.

Using the Stealth Conditionals and and or

The conditionals and and or are simple mathematical operators, which allow you to manipulate Boolean values in the same way you might manipulate numbers using addition and subtraction.

For example, here’s how we could use and to see if three numbers are odd:

> (and (oddp 5) (oddp 7) (oddp 9))

T

Because 5, 7, and 9 are odd, the entire expression evaluates as true.

Similarly, we can use or to see whether at least one of a set of numbers is odd:

> (or (oddp 4) (oddp 7) (oddp 8))

T

Because 7 is odd, the or command still evaluates as true, despite the fact that 4 and 8 are even.

But there’s something a bit more interesting about and and or that you might not notice just by looking at these first two examples. So far, these two commands look like completely ordinary mathematical operators; they do not look like conditional commands, such as if or cond. However, they can be used for conditional behavior.

For instance, here’s how we could use these conditionals to set a global variable to true only when a number is even:

> (defparameter *is-it-even* nil)

*IS-IT-EVEN*

> (or (oddp 4) (setf *is-it-even* t))

T

> *is-it-even*

T

If we do the same thing using an odd number, the variable remains unchanged:

> (defparameter *is-it-even* nil)

*IS-IT-EVEN

> (or (oddp 5) (setf *is-it-even* t))

T

> *is-it-even*

NIL

This example illustrates that Lisp uses shortcut Boolean evaluation. This means that once Lisp determines that an earlier statement in a list of or values is true, it simply returns true and doesn’t bother evaluating the remaining statements. Similarly, once it determines that an earlier statement in a list of and values is false, it stops without bothering to evaluate the rest of the statements.

While this may seem like a minor esoteric observation, it can actually be very useful in many situations. For instance, imagine if you want to save a file to disk, but only if the file was modified, and only when the user wants it to be saved. The basic structure could be written as follows:

(if *file-modified*
    (if (ask-user-about-saving)
        (save-file)))

Here, the function ask-user-about-saving would ask the user about the file, and then return true or false based on the user’s wishes. However, since shortcut Boolean evaluation is guaranteed to be used for Boolean operations under Common Lisp and most other Lisp dialects, we could write this instead:

(and *file-modified* (ask-user-about-saving) (save-file))

Using this cleaner style for evaluating conditional code is possible only if you think beyond the typical use of the Boolean operators as simply mathematical operators. This form has an elegant symmetry between the three expressions, which some Lispers may like. However, others would argue that a reader of your code may easily miss the fact that (save-file) does something beyond returning a Boolean value. A bit of time is required to wrap your head around this more-general conception of what and and or actually mean.

A third way to write this code, which is a compromise between the previous approaches, is as follows:

(if (and *file-modified*
         (ask-user-about-saving))
    (save-file)))

Many experienced Lispers will consider this version a bit clearer than the previous two versions, because only expressions that are expressly designed to return a Boolean value are treated as part of the condition.

Using Functions That Return More than Just the Truth

Now let’s look at another benefit of Lisp’s simple way of thinking about true and false. As we’ve already discussed, any value in Common Lisp (except for the different variations on nil) is true. This means that functions that are commonly used in conditions have the option of returning more than just the truth.

For instance, the Lisp command member can be used to check for list membership for an item:

> (if (member 1 '(3 4 1 5))
      'one-is-in-the-list
      'one-is-not-in-the-list)

'ONE-IS-IN-THE-LIST

This seems pretty straightforward. However, once again, there is something happening behind the scenes that you may not expect. Let’s run the member command in isolation:

> (member 1 '(3 4 1 5))

(1 5)

What the heck happened here? Why is it returning (1 5)?

Actually, there’s a perfectly rational explanation for this. Whenever a Lisper writes a function that returns true and false, she will think to herself, “Is there anything else I could return other than just t?” Since all non-nil values in Common Lisp evaluate to true, returning some other value is essentially a freebie. The implementers of the member function decided that some crazy Lisper somewhere may see the value in having the tail of the list for some calculation that uses this function.

Note

Remember from Chapter 3 that the list '(3 4 1 5) is the same as the nested contraption (cons 3 (cons 4 (cons 1 (cons 5 nil)))). This should make it clear why the value (cons 1 (cons 5 nil)) is an easy thing for the member function to return.

But why doesn’t it just return the value it found, instead of the tail? In fact, this would have been a useful way to define the member function, because it would allow passing the original value to some other function in such a manner. Unfortunately, one edge case in particular would ruin this plan:

> (if (member nil '(3 4 nil 5))
      'nil-is-in-the-list
      'nil-is-not-in-the-list)

'nil-is-in-the-list

As you can see in this example, the member function still gives the correct answer, even when we search for nil as the member! If the member function had actually returned nil (in other words, the original value we were searching for), it would have evaluated as false, and the example would have incorrectly stated that nil isn’t in the list. However, since the member function returns the tail of the list at the point of the found item, it can be guaranteed to always be a true value. A successful discovery of the desired value will always return a list with at least one value, which we know always evaluates as true.

One function that really benefits from rich return values is find-if, as follows:

> (find-if #'oddp '(2 4 5 6))

5

> (if (find-if #'oddp '(2 4 5 6))
      'there-is-an-odd-number
      'there-is-no-odd-number)

'there-is-an-odd-number

The find-if function actually takes another function, in this case oddp, as a parameter. find-if will find the first value in the list for which oddp returns true. In this case, it will find the first number (if any) that is an odd number.

You can see clearly how find-if can fill dual roles: either as a retriever of values matching some constraint or as a true/false value inside a condition.

Note

Don’t worry yet about the weird hash mark (#) in front of oddp in the example. We’ll discuss the find-if function, and other so-called higher-order functions, in greater detail in Chapter 7 and Chapter 14.

Alas, the elegant symmetry of the find-if function has a single, small, ugly wart. If we try our edge case again, searching for a nil value, we get a rather disappointing result:

> (find-if #'null '(2 4 nil 6))

NIL

The null function, which returns true for any of the nil values, correctly finds the nil. Unfortunately, in this one annoying case, we would not want to use find-if inside a conditional statement, because a correctly found value still returns a result that evaluates as false. The symmetry has been broken.

These are the kinds of small things that make even grown Lispers shed a tear.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.31.22