Welcome to Clith!
- Introduction
- Installation
- Reference
- Getting started
- Defining a WITH expansion
- Documentation
- Declarations
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!"
- Manual:
cd ~/common-lisp
git clone https://github.com/Hectarea1996/clith.git
- Quicklisp:
(ql:quickload "clith")
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
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
.
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)))
...)
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
74EC
(list x y z a b c)))
;; Returns
(#:X329 #:Y330 #:Z331 #:CUSTOM-A332 #:CUSTOM-B333 #:C334)
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!"
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.