Welcome to Clith!
- Introduction
- Installation
- Reference
- Getting started
- Defining a WITH expansion
- Documentation
- Declarations
This library defines the macro clith:with and a more relaxed version clith:with*.
These macros aim to encapsulate every kind of WITH- macro into one.
(with ((file (open "~/file.txt" :direction :output)))
(print "Hello Clith!" file))clith:with is powerful enough to support almost every WITH- macro:
(defwith slots (vars (object) body)
`(with-slots ,vars ,object
,@body))
(defstruct 3d-vector x y z)
;; WITH* accepts regular bindings
(with* ((p (make-3d-vector :x 1 :y 2 :z 3))
((z (up y) x) (slots p)))
(+ x up z));; Returns
6It 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
3And 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")The macros clith:with and clith:with* uses WITH expansions in a similar way to setf. These expansions control how these macros are 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
NILclith:with* can be used as let or multiple-value-bind as well:
(with* (x
(y 3)
((q r) (floor 4 5)))
(values x y q r));; Returns
NIL
3
0
4Every 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
TIn 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 clith:with and/or clith: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-WINDOWThis 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
GENSYMSEach 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
(#:X341 #:Y342 #:Z343 #:CUSTOM-A344 #:CUSTOM-B345 #:C346)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 ((#:G358 (MAKE-WINDOW "Window 1")))
(UNWIND-PROTECT
(LET ((W1 #:G358))
(DECLARE (IGNORABLE W1))
(LET ((#:G355 (MAKE-WINDOW "Window 2")))
(UNWIND-PROTECT
(LET ((W2 #:G355))
(DECLARE (IGNORABLE W2))
(PRINT "Hello world!"))
(DESTROY-WINDOW #:G355))))
(DESTROY-WINDOW #:G358)))
TObserve 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.
On the other side, while defining a new WITH expansion, declarations are included in the body argument. All the examples above consider that body can contain declarations. However, an extra optional argument can be specified on clith:defwith to receive declarations separately.
Taking back the MAKE-WINDOW example, the following two definitions are equivalent:
(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) ; <-- Body containing declarations
(destroy-window ,window-var)))))
(defwith make-window ((window) (title) body declarations) ; <-- Receiving declarations separately
"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))
,@declarations ; <-- Expanding declarations here
,@body)
(destroy-window ,window-var)))))