Skip to content

LISP - Writing Data to Streams

Writing data is a fundamental I/O operation. In Common Lisp, all output is directed to streams. A stream is a source or sink of data, such as the console, a file, or a string. This tutorial explores modern best practices for writing data.

Lisp manages several standard streams. The most important one for output is *standard-output*, which typically defaults to your terminal or console (REPL).

Core Output Functions: Choosing the Right Tool

Section titled “Core Output Functions: Choosing the Right Tool”

Lisp provides several functions for writing. The key is to choose the one that matches your intent.

  • princ: Writes an object in a human-readable format. For example, it prints strings without the surrounding quotes. Use this for generating clean output for users. (princ "Hello, World!") -> Hello, World!
  • print: Writes an object in a Lisp-readable format, preceded by a newline and followed by a space. It includes quotes for strings and is suitable for debugging or output that Lisp’s read function could potentially parse back. (print "Hello, Lisp!") -> (newline)"Hello, Lisp!"
  • write: The master function, highly customizable with keyword arguments. print and princ are essentially convenient presets for write. Use write when you need fine-grained control over the output format.
  • format: The most powerful and flexible tool. It creates formatted text using a control string and arguments, similar to printf in C. It’s the ideal choice for composing complex messages, reports, or any structured text.

Practical Example 1: Writing to the Console with format

Section titled “Practical Example 1: Writing to the Console with format”

format is the idiomatic way to produce structured output. The first argument t directs output to *standard-output*.

(let ((user "Alex")
(items-count 5))
(format t "Welcome, ~a!~%You have ~d items in your cart.~%" user items-count))
;; Directives:
;; ~a - Aesthetic: prints in a human-readable way (like princ).
;; ~d - Decimal: prints an integer.
;; ~% - Tilde-percent: prints a newline character.
Welcome, Alex!
You have 5 items in your cart.

The modern, safe, and recommended way to write to a file is with the with-open-file macro. It automatically handles opening the file and, crucially, ensures the stream is closed when the block is exited, even if an error occurs.

;; Create a log file named 'app.log'
(with-open-file (log-stream "app.log"
:direction :output
:if-exists :supersede
:if-does-not-exist :create)
(format log-stream "Log started at ~a~%" (get-universal-time))
(princ "Application event: User logged in." log-stream)
(terpri log-stream) ; terpri = terminate print (writes a newline)
(format log-stream "Log finished."))
(print "Log file 'app.log' has been written.")
Log started at 3855138000
Application event: User logged in.
Log finished.
  • Use with-open-file: Always prefer this macro over manually calling open and close for file I/O. It prevents resource leaks.
  • Buffering: Output to streams is often buffered for efficiency. If you need to ensure data is written immediately (e.g., to a log file before a potential crash), call (finish-output stream) or (force-output stream).
  • Writing to Strings: To capture output into a string instead of a file or console, use the with-output-to-string macro. Example: (with-output-to-string (s) (format s "Hello, ~a" "string")) returns "Hello, string".
  • Error Handling: File operations can fail (e.g., disk full, no permissions). Wrap with-open-file in a handler-case block to gracefully handle potential file-error conditions in robust applications.