*FEATURES* and Read-Time Conditionalization

Before you can implement this API in a library that will run correctly on multiple Common Lisp implementations, I need to show you the mechanism for writing implementation-specific code.

While most of the code you write can be “portable” in the sense that it will run the same on any conforming Common Lisp implementation, you may occasionally need to rely on implementation-specific functionality or to write slightly different bits of code for different implementations. To allow you to do so without totally destroying the portability of your code, Common Lisp provides a mechanism, called read-time conditionalization, that allows you to conditionally include code based on various features such as what implementation it’s being run in.

The mechanism consists of a variable ***FEATURES*** and two extra bits of syntax understood by the Lisp reader. ***FEATURES*** is a list of symbols; each symbol represents a “feature” that’s present in the implementation or on the underlying platform. These symbols are then used in feature expressions that evaluate to true or false depending on whether the symbols in the expression are present in ***FEATURES***. The simplest feature expression is a single symbol; the expression is true if the symbol is in ***FEATURES*** and false if it isn’t. Other feature expressions are boolean expressions built out of **NOT**, **AND**, and **OR** operators. For instance, if you wanted to conditionalize some code to be included only if the features foo and bar were present, you could write the feature expression (and foo bar).

The reader uses feature expressions in conjunction with two bits of syntax, #+ and #-. When the reader sees either of these bits of syntax, it first reads a feature expression and then evaluates it as I just described. When a feature expression following a #+ is true, the reader reads the next expression normally. Otherwise it skips the next expression, treating it as whitespace. #- works the same way except it reads the form if the feature expression is false and skips it if it’s true.

The initial value of ***FEATURES*** is implementation dependent, and what functionality is implied by the presence of any given symbol is likewise defined by the implementation. However, all implementations include at least one symbol that indicates what implementation it is. For instance, Allegro Common Lisp includes the symbol :allegro, CLISP includes :clisp, SBCL includes :sbcl, and CMUCL includes :cmu. To avoid dependencies on packages that may or may not exist in different implementations, the symbols in ***FEATURES*** are usually keywords, and the reader binds ***PACKAGE*** to the **KEYWORD** package while reading feature expressions. Thus, a name with no package qualification will be read as a keyword symbol. So, you could write a function that behaves slightly differently in each of the implementations just mentioned like this:

  1. (defun foo ()
  2. #+allegro (do-one-thing)
  3. #+sbcl (do-another-thing)
  4. #+clisp (something-else)
  5. #+cmu (yet-another-version)
  6. #-(or allegro sbcl clisp cmu) (error "Not implemented"))

In Allegro that code will be read as if it had been written like this:

  1. (defun foo ()
  2. (do-one-thing))

while in SBCL the reader will read this:

  1. (defun foo ()
  2. (do-another-thing))

while in an implementation other than one of the ones specifically conditionalized, it will read this:

  1. (defun foo ()
  2. (error "Not implemented"))

Because the conditionalization happens in the reader, the compiler doesn’t even see expressions that are skipped.1 This means you pay no runtime cost for having different versions for different implementations. Also, when the reader skips conditionalized expressions, it doesn’t bother interning symbols, so the skipped expressions can safely contain symbols from packages that may not exist in other implementations.

Packaging the Library

Speaking of packages, if you download the complete code for this library, you’ll see that it’s defined in a new package, com.gigamonkeys.pathnames. I’ll discuss the details of defining and using packages in Chapter 21. For now you should note that some implementations provide their own packages that contain functions with some of the same names as the ones you’ll define in this chapter and make those names available in the CL-USER package. Thus, if you try to define the functions from this library while in the CL-USER package, you may get errors or warnings about clobbering existing definitions. To avoid this possibility, you can create a file called packages.lisp with the following contents:

  1. (in-package :cl-user)
  1. (defpackage :com.gigamonkeys.pathnames
  2. (:use :common-lisp)
  3. (:export
  4. :list-directory
  5. :file-exists-p
  6. :directory-pathname-p
  7. :file-pathname-p
  8. :pathname-as-directory
  9. :pathname-as-file
  10. :walk-directory
  11. :directory-p
  12. :file-p))

and **LOAD** it. Then at the REPL or at the top of the file where you type the definitions from this chapter, type the following expression:

  1. (in-package :com.gigamonkeys.pathnames)

In addition to avoiding name conflicts with symbols already available in CL-USER, packaging the library this way also makes it easier to use in other code, as you’ll see in several future chapters.