Welcome to Clith!
- Introduction
- Installation
- Getting started
- Defining a WITH expansion
- Documentation
- Declarations
- Built-in WITH expansions
- Reference
This library defines the macro clith:with.
This macro aims 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 body object)
`(with-slots ,vars ,object
,@body))
(defstruct 3d-vector x y z)
(let ((p (make-3d-vector :x 1 :y 2 :z 3)))
(with (((z (up y) x) (slots p)))
(+ x up z)));; Returns
6It supports declarations:
(let ((p (make-3d-vector :x 1 :y 2 :z 3)))
(with (((x y z) (slots p)))
(declare (ignore x z))
(values y)));; Returns
2And it detects macros and symbol-macros:
(symbol-macrolet ((my-file (open "~/file.txt")))
(with ((f my-file))
(read f)));; Returns
"Hola mundo"- Manual:
cd ~/common-lisp
git clone https://github.com/HectareaGalbis/clith.git- Quicklisp:
(ql:quickload "clith")The macro clith:with uses WITH expansions in a similar way to setf. These expansions control how this macro 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
NILEvery 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 in order to use both functions. Let's define the WITH expansion:
(defwith make-window ((window) body title)
"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
(#:X281 #:Y282 #:Z283 #:CUSTOM-A284 #:CUSTOM-B285 #:C286)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 ((#:G298 (MAKE-WINDOW "Window 1")))
(UNWIND-PROTECT
(LET ((W1 #:G298))
(DECLARE (IGNORABLE W1))
(LET ((#:G297 (MAKE-WINDOW "Window 2")))
(UNWIND-PROTECT
(LET ((W2 #:G297))
(DECLARE (IGNORABLE W2))
(PRINT "Hello world!"))
(DESTROY-WINDOW #:G297))))
(DESTROY-WINDOW #:G298)))
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.
The following Common Lisp functions have a WITH expansion:
make-broadcast-streammake-concatenated-streammake-echo-streammake-string-input-streammake-string-output-streammake-synonym-streammake-two-way-streamopen
Define a WITH expansion. A WITH expansion controls how the macro WITH is expanded. This macro has
the following syntax:
(DEFWITH name (vars args with-body [with-declaration]) declaration* body*)
name ::= symbol
args ::= macro-lambda-list
body ::= form
When using (NAME ARGS*) inside the macro WITH, it will expand to the value returned by DEFWITH.
ARGS must indicate at least 2 required arguments being:
1. The list of variables to bound. Each element of the list can have the form {var | (var var-option*)} where
var is a symbol and var-option can be any form.
2. The body of the WITH macro.
Keep in mind that the second argument can contain declarations.
As an example, let's define the with expansion MY-FILE. We will make WITH to be expanded to WITH-OPEN-FILE.
(defwith my-file ((stream) body filespec &rest options)
"Open a file."
`(with-open-file (,stream ,filespec ,@options)
,@body))
In this example, as VARS is always a list, we can use destructuring to retrieve directly the variable to bound.
Also, we are assuming here that no additional options are passed with the variable.
Now, using WITH:
(with ((file (my-file "~/file.txt" :direction :output)))
(print "Hey!" file))
Finally, note that we put a docstring when we defined MY-FILE. We can retrieve it with DOCUMENTATION:
(documentation 'my-file 'with) ;; --> "Open a file."
This macro has the following systax:
(WITH (binding*) declaration* form*)
binding ::= ([vars] form)
vars ::= symbol | (var-with-options*)
var-with-options ::= symbol | (symbol var-option*)
var-option ::= form
WITH accepts a list of binding clauses. Each binding clause can be a symbol or a list. Depending on
this, the behaeviour of WITH is slightly different:
- A list with one element. That element must be a WITH expansion. The expansion is expanded
according to DEFWITH. In this case, the WITH expansion will receive NIL as the list of variables to bound.
(with (((init-video-system))) ; Possible expansion that should finalize the video system at the end
;; Doing video stuff
)
- A list with two elements: The first element must be a symbol or a list of symbols with
or without options. The second element must be a WITH expansion:
(with ((my-file (open "~/my-file.txt"))) ; Expanded to WITH-OPEN-FILE
...)
Each variable in a binding clause can have options. These options should be used inside DEFWITH
to control the expansion with better precision:
(defwith slots (vars (object) body)
`(with-slots ,vars ,object
,@body))
(defstruct 3d-vector x y z)
(with ((v (make-3d-vector :x 1 :y 2 :z 3))
((x (up y) z) (slots v)))
(+ x up z))
Macros and symbol-macros are treated specially. If a macro or symbol-macro is used, they
will be expanded with MACROEXPAND-1 and its result must be a WITH expansion.
Checks wether a symbol denotes a WITH expansion.