Skip to content

Latest commit

 

History

86 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Common Lisp wITH

Welcome to Clith!

Introduction

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
6

It 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
2

And it detects macros and symbol-macros:

(symbol-macrolet ((my-file (open "~/file.txt")))
  (with ((f my-file))
    (read f)))
;; Returns
"Hola mundo"

Installation

  • Manual:
cd ~/common-lisp
git clone https://github.com/HectareaGalbis/clith.git
  • Quicklisp:
(ql:quickload "clith")

Getting started

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
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 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-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
(#:X281 #:Y282 #:Z283 #:CUSTOM-A284 #:CUSTOM-B285 #:C286)

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 ((#: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)))
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.

Built-in WITH expansions

The following Common Lisp functions have a WITH expansion:

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

Reference

Macro: clith:defwith (name args &body body)

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."

Macro: clith:with (bindings &body body)

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.

Function: clith:withp (sym)

Checks wether a symbol denotes a WITH expansion.

About

Common Lisp wITH macro.

Resources

Stars

6 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages