clith
2025-06-22
Common Lisp wITH macro. A general WITH macro.
Common Lisp wITH
Welcome to Clith!
- Introduction
- Installation
- Reference
- Getting started
- Defining a WITH expansion
- Documentation
- Declarations
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.