Streams are usually used for communicating with the outside world from within a Lisp program. One exception to this is the string stream, which simply makes a string look like a stream. In the same way you can read or write to external resources with other types of streams, a string stream will let you read or write to a string.
You can create string streams with the make-string-output-stream
and make-string-input-stream
commands. Following is an example that uses make-string-output-stream
:
>(defparameter foo (make-string-output-stream))
>(princ "This will go into foo. " foo)
>(princ "This will also go into foo. " foo)
>(get-output-stream-string foo)
"This will go into foo. This will also go into foo. "
You may be wondering why anyone would want to do this, since we can already directly manipulate strings in Lisp, without using streams. Actually, there are several good reasons for using string streams in this way. They are useful for debugging, as well as for creating complex strings efficiently.
Using string streams allows us to use functions that require streams as parameters. This is great for debugging code that works with files or sockets, using only strings for the input and output of data.
For example, suppose we have a function write-to-log
that writes log information to a stream. Usually, we would want to send the log information to a file stream, so it can be written to a file for safekeeping. However, if we want to debug the function, we may want to send it a string stream instead, so we can take a look at the data it writes and make sure it’s correct. If we had hard-coded the write-to-log
function to only write to a file, we wouldn’t have this flexibility. This is why it makes sense to write functions to use the abstract concept of a stream whenever possible, instead of using other methods to access external resources.
String streams can lead to better-performing code when dealing with very long strings. For instance, concatenating two strings together can be a costly operation—first, it requires a new block of memory to be allocated to hold both strings, and then the strings need to be copied into this new location. Because of this bottleneck, many programming languages use devices called string builders to avoid this overhead. In Lisp, we can get similar performance benefits by using string streams.
Another reason for using string streams is that they can make our code easier to read and debug, especially when we use the with-output-to-string
macro.
Here’s an example of this command being used:
>(with-output-to-string (*standard-output*)
(princ "the sum of ")
(princ 5)
(princ " and ")
(princ 2)
(princ " is ")
(princ (+ 2 5)))
"the sum of 5 and 2 is 7"
The with-output-to-string
macro will intercept any text that would otherwise be output to the console, REPL, or other output stream, and capture it as a string. In the preceding example, the output created by the princ
functions within the body of the with-output-to-string
call is redirected automatically into a string stream. Once the body of the with-output-to-string
command has completed, the entire printed output that was put into the stream is returned as a result .
You can also use the with-output-to-string
macro to easily construct complex strings by “printing” each part, and then capturing the result as a string. This tends to be much more elegant and efficient than using the concatenate
command.
Using with-output-to-string
runs counter to the tenets of functional programming (discussed in Chapter 14). Some Lispers consider this function (and similar functions that intercept input or output intended for other destinations) to be an ugly hack. You’ll see some disagreement in the Lisp community about whether the use of with-output-to-string
is elegant or ugly.
3.138.105.215