/clith

Common Lisp wITH macro.

Primary LanguageCommon LispMIT LicenseMIT

Common Lisp wITH

Welcome to Clith!

This library defines the macro clith:with. It allows you to create some objects, bind them to some variables, evaluate some expressions using these variables, and lastly the objects are destroyed automatically.

Installation

  • Manual:
cd ~/common-lisp
git clone https://github.com/Hectarea1996/clith.git
  • Quicklisp (Ultralisp):
(ql-dist:install-dist "http://dist.ultralisp.org/" :prompt nil)
(ql:quickload "clith")

Reference

Basic usage

The macro clith:with uses WITH expanders simarly setf uses setf expanders. These expanders controls how the macro clith:with is expanded.

Let's take a look at the built-in slots expander. As the name suggest, it will expand into a with-slots expression:

(defstruct vec2
  x
  y)

(let ((start (make-vec2 :x 5 :y 10)))
  (with (((x y) (slots start)))
    (+ x y)))
15

The macro clith:with also accepts options for each variable we want to bind. In the above example, what happens if we have two points?

(let ((start (make-vec2 :x 5 :y 10))
      (end   (make-vec2 :x -3 :y -4)))
  (with (((x y) (slots origin))
         ((x y) (slots end)))   ;; <-- Name collision!!
    (+ x y x y)))

We should specify, as if using with-slots, that we want to reference the slot x or y using another symbol.

(let ((start (make-vec2 :x 5 :y 10))
      (end   (make-vec2 :x -3 :y -4)))
  (with ((((x1 x) (y1 y)) (slots start))
         (((x2 x) (y2 y)) (slots end)))
    (+ x1 y1 x2 y2)))
8

Defining a WITH expander

In order to extend the macro clith:with we need to define a WITH expander. 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 expander:

(defwith make-window ((window) (title) &body body)
  "Makes a window that will be destroyed after the end of WITH."
  (let ((window-var (gensym)))
    `(let ((,window-var (make-window ,title)))
       (let ((,window ,window-var))
         (unwind-protect
             (progn ,@body)
           (destroy-window ,window-var))))))
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 expander in WITH:

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

After the body of clith:with is evaluated, my-window will be destroyed by destroy-window.

Expander's 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)
"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)
"Another docstring!"

Declarations

The macro clith:with accepts declarations. These declarations are moved to the correct place at expansion time. For example, consider again the example with the points, but this time, we want to ignore two arguments:

(let ((start (make-vec2 :x 5 :y 10))
      (end   (make-vec2 :x -3 :y -4)))
  (with ((((x1 x) (y1 y)) (slots start))
         (((x2 x) (y2 y)) (slots end)))
    (declare (ignore y1 x2))
    (+ x1 y2)))
1

Let's see the expanded code:

(macroexpand-1 '(with ((((x1 x) (y1 y)) (slots start))
                       (((x2 x) (y2 y)) (slots end)))
                  (declare (ignore y1 x2))
                  (+ x1 y2)))
(with-slots ((x1 x) (y1 y))
    start
  (declare (ignore y1))
  (with-slots ((x2 x) (y2 y))
      end
    (declare (ignore x2))
    (+ x1 y2)))

t

Observe that every declaration is in the right place. But how this work?

clith:with assumes that variables to be bound will be in certain places. Each variable in the declaration is searched over all the places that can contain a variable to be bound. It is searched from bottom to top. When a variable is found, a declaration of that variable is created there.

If you want to know exactly where these places are, check out the syntax of the clith:with macro:

(WITH (binding*) declaration* form*)

binding          ::= ([vars] form)
vars             ::= var | (var-with-options*)
var-with-options ::= var | (var var-option*)
var-option       ::= form

var are those places where a declaration can refer to.

Built-in WITH expanders

The next symbols from the package CL has a built-in expander:

  • make-broadcast-stream
  • make-concatenated-stream
  • make-echo-stream
  • make-string-input-stream
  • make-string-output-stream
  • make-two-way-stream
  • open

Additionally, every macro from the package CL whose name starts with with- has its own expander. We've already seen an example using the expander slots.

Since we cannot define new symbols in the package CL, these expanders are defined in a special way. clith:with will recognize all the symbols (for any package) whose name is equal to the name of the expander.

The complete list is:

CL Standard macro WITH expander
with-accesors accesors
with-compilation-unit compilation-unit
with-condition-restarts condition-restarts
with-hash-table-iterator hash-table-iterator
with-input-from-string input-from-string
with-open-file open-file
with-open-stream open-stream
with-output-to-string output-to-string
with-package-iterator package-iterator
with-simple-restart simple-restart
with-slots slots
with-standard-io-syntax standard-io-syntax