STREAM
is an output stream that sends any data written to it to a set of output streams provided as arguments to its constructor function, MAKE-BROADCAST- STREAM
.[164] Conversely, a CONCATENATED-STREAM
is an input stream that takes its input from a set of input streams, moving from stream to stream as it hits the end of each stream. CONCATENATED-STREAM
s are constructed with the function MAKE-CONCATENATED-STREAM
, which takes any number of input streams as arguments.
Two kinds of bidirectional streams that can plug together streams in a couple ways are TWO-WAY-STREAM
and ECHO-STREAM
. Their constructor functions, MAKE-TWO-WAY-STREAM
and MAKE-ECHO-STREAM
, both take two arguments, an input stream and an output stream, and return a stream of the appropriate type, which you can use with both input and output functions.
In a TWO-WAY-STREAM
every read you perform will return data read from the underlying input stream, and every write will send data to the underlying output stream. An ECHO-STREAM
works essentially the same way except that all the data read from the underlying input stream is also echoed to the output stream. Thus, the output stream of an ECHO-STREAM
stream will contain a transcript of both sides of the conversation.
Using these five kinds of streams, you can build almost any topology of stream plumbing you want.
Finally, although the Common Lisp standard doesn't say anything about networking APIs, most implementations support socket programming and typically implement sockets as another kind of stream, so you can use all the regular I/O functions with them.[165]
Now you're ready to move on to building a library that smoothes over some of the differences between how the basic pathname functions behave in different Common Lisp implementations.
15. Practical: A Portable Pathname Library
As I discussed in the previous chapter, Common Lisp provides an abstraction, the pathname, that's supposed to insulate you from the details of how different operating systems and file systems name files. Pathnames provide a useful API for manipulating names as names, but when it comes to the functions that actually interact with the file system, things get a bit hairy.
The root of the problem, as I mentioned, is that the pathname abstraction was designed to represent filenames on a much wider variety of file systems than are commonly used now. Unfortunately, by making pathnames abstract enough to account for a wide variety of file systems, Common Lisp's designers left implementers with a fair number of choices to make about how exactly to map the pathname abstraction onto any particular file system. Consequently, different implementers, each implementing the pathname abstraction for the same file system, just by making different choices at a few key junctions, could end up with conforming implementations that nonetheless provide different behavior for several of the main pathname-related functions.
However, one way or another, all implementations provide the same basic functionality, so it's not too hard to write a library that provides a consistent interface for the most common operations across different implementations. That's your task for this chapter. In addition to giving you several useful functions that you'll use in future chapters, writing this library will give you a chance to learn how to write code that deals with differences between implementations.
The basic operations the library will support will be getting a list of files in a directory and determining whether a file or directory with a given name exists. You'll also write a function for recursively walking a directory hierarchy, calling a given function for each pathname in the tree.
In theory, these directory listing and file existence operations are already provided by the standard functions DIRECTORY
and PROBE-FILE
. However, as you'll see, there are enough different ways to implement these functions—all within the bounds of valid interpretations of the language standard—that you'll want to write new functions that provide a consistent behavior across implementations.
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
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 *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