clith

2025-06-22

Common Lisp wITH macro. A general WITH macro.

Upstream URL

github.com/HectareaGalbis/clith

Author

Héctor Galbis Sanchis

License

MIT
README

Common Lisp wITH

Welcome to Clith!

Introduction

This library defines the macro clith:with. This macro binds symbols to objects that can be finalized automatically in a personalized way.

(with ((file (open "~/file.txt" :direction :output)))
  (print "Hello Clith!" file))

clith:with is powerful enough to support almost every regular WITH- macro:

(defwith slots (vars (object) body)
  `(with-slots ,vars ,object
     ,@body))

(defstruct 3d-vector x y z)

(with ((p (make-3d-vector :x 1 :y 2 :z 3))
       ((z (up y) x) (slots p)))
  (+ x up z))
;; Returns
6

It supports declarations:

(with (((x y z) (values 1 2 3))
       ((a b c) (values 'a 'b 'c)))
  (declare (ignore a y c))
  (values x b z))
;; Returns
1
B
3

And it detects macros and symbol-macros:

(symbol-macrolet ((my-file (open "~/file.txt")))
  (with ((f my-file))
    (read f)))
;; Returns
"Hello Clith!"

Installation

  • Manual:
cd ~/common-lisp
git clone https://github.com/Hectarea1996/clith.git
  • Quicklisp:
(ql:quickload "clith")

Reference

Getting started

clith:with can be used as let or multiple-value-bind:

(with (x
       (y 3)
       ((q r) (floor 4 5)))
  (values x y q r))
;; Returns
NIL
3
0
4

The macro clith:with uses WITH expansions in a similar way to setf. These expansions control how the macro clith:with is expanded.

(let (some-stream)

  (with ((the-stream (open "~/test.txt")))
    (setf some-stream the-stream)
    (format t "Stream opened? ~s~%" (open-stream-p some-stream)))

  (format t "Stream opened after? ~s" (open-stream-p some-stream)))
;; Output
Stream opened? T
Stream opened after? NIL
;; Returns
NIL

Every Common Lisp function that creates an object that should be closed/destroyed has a WITH expansion defined by CLITH. For example, functions like open or make-two-way-stream have a WITH expansion. See all the functions in the reference.

Also, we can check if a symbol denotes a WITH expansion using clith:withp:

(withp 'open)
;; Returns
T

Defining a WITH expansion

Simple example: MAKE-WINDOW

In order to extend the macro clith:with we need to define a WITH expansion. To do so, we use clith:defwith.

Suppose we have (MAKE-WINDOW TITLE) and (DESTROY-WINDOW WINDOW). We want to control the expansion of WITH in order to use both functions. Let's define the WITH expansion:

(defwith make-window ((window) (title) body)
  "Makes a window that will be destroyed after the end of WITH."
  (let ((window-var (gensym)))
    `(let ((,window-var (make-window ,title)))
       (unwind-protect
           (let ((,window ,window-var))
             ,@body)
         (destroy-window ,window-var)))))
;; Returns
MAKE-WINDOW

This is a common implementation of a WITH- macro. Note that we specified (window) to specify that only one variable is wanted.

Now we can use our expansion:

(with ((my-window (make-window "My window")))
  ;; Doing things with the window
  )

After the evaluation of the body, my-window will be destroyed by destroy-window.

No need to return a value: INIT-SUBSYSTEM

There are WITH- macros that doesn't return anything. They just initialize something that should be finalized at the end. Imagine that we have the functions INIT-SUBSYSTEM and FINALIZE-SUBSYSTEM. Let's define a WITH expansion that calls to FINALIZE-SUBSYSTEM:

(defwith init-subsystem (() () body) ; <- No variables to bind and no arguments.
  "Initialize the subsystem and finalize it at the end of WITH."
  `(progn
     (init-subsystem)
     (unwind-protect
         (progn ,@body)
       (finalize-subsystem))))

Now we don't need to worry about finalizing the subsystem:

(with (((init-subsystem)))
  ...)

Extended syntax: GENSYMS

Some WITH- macros like with-slots allow to specify some options to variables. Let's try to make a WITH expansion that works like alexandria:with-gensyms. Each variable should optionally accept the prefix for the fresh generated symbol.

We want to achieve something like this:

(with ((sym1 (gensyms))                  ; <- Regular syntax
       ((sym2 (sym3 "FOO")) (gensyms)))  ; <- Extended syntax for SYM3
  ...)

In order to do this, we are using gensym:

(defwith gensyms (vars () body)
  (let* ((list-vars (mapcar #'alexandria:ensure-list vars))
         (sym-vars (mapcar #'car list-vars))
         (prefixes (mapcar #'cdr list-vars))
         (let-bindings (mapcar (lambda (sym-var prefix)
                                 `(,sym-var (gensym ,(if prefix (car prefix) (symbol-name sym-var)))))
                               sym-vars prefixes)))
    `(let ,let-bindings
       ,@body)))
;; Returns
GENSYMS

Each element in VARS can be a symbol or a list. That's the reason we are using alexandria:ensure-list. LIST-VARS will contain lists where the first element is the symbol to bound and can have a second element, the prefix. We store then the symbols in SYM-VARS and the prefixes in PREFIXES. Note that if a prefix is not specified, then the corresponding element in PREFIXES will be NIL. If some PREFIX is NIL, we use the name of the respective SYM-VAR. Finally, we create the LET-BINDING and use it in the final form.

Let's try it out:

(with ((x (gensyms))
       ((y z) (gensyms))
       (((a "CUSTOM-A") (b "CUSTOM-B") c) (gensyms)))
  (values (list x y z a b c)))
;; Returns
(#:X329 #:Y330 #:Z331 #:CUSTOM-A332 #:CUSTOM-B333 #:C334)

Documentation

The macro clith:defwith accepts a docstring that can be retrieved with the function documentation. Check out again the definition of the expansion of make-window above. Note that we wrote a docstring.

(documentation 'make-window 'with)
;; Returns
"Makes a window that will be destroyed after the end of WITH."

We can also setf the docstring:

(setf (documentation 'make-window 'with) "Another docstring!")
(documentation 'make-window 'with)
;; Returns
"Another docstring!"

Declarations

The macro clith:with accepts declarations. These declarations are moved to the correct place at expansion time. For example, imagine we want to open two windows, but the variables can be ignored:

(with ((w1 (make-window "Window 1"))
       (w2 (make-window "Window 2")))
  (declare (ignorable w1 w2))
  (print "Hello world!"))

Let's see the expanded code:

(macroexpand-1 '(with ((w1 (make-window "Window 1"))
                       (w2 (make-window "Window 2")))
                  (declare (ignorable w1 w2))
                  (print "Hello world!")))
;; Returns
(LET ((#:G346 (MAKE-WINDOW "Window 1")))
  (UNWIND-PROTECT
      (LET ((W1 #:G346))
        (DECLARE (IGNORABLE W1))
        (LET ((#:G343 (MAKE-WINDOW "Window 2")))
          (UNWIND-PROTECT
              (LET ((W2 #:G343))
                (DECLARE (IGNORABLE W2))
                (PRINT "Hello world!"))
            (DESTROY-WINDOW #:G343))))
    (DESTROY-WINDOW #:G346)))
T

Observe that the declarations are in the right place. Every symbol that can be bound is a candidate for a declaration. If more that one candidate is found (same symbol appearing more than once) the last one is selected.

Dependencies (2)

  • alexandria
  • expanders

Dependents (0)

    • GitHub
    • Quicklisp