destructuring extension of let*
Common Lisp
Latest commit 77efafd Jan 9, 2017 @tpapp committed on GitHub Merge pull request #9 from scymtym/wip-flet+-ignore
Handle &IGN in lambda lists of &FLET+, &LABELS+, DEFUN+, etc. correctly.

(Thanks for the fix!)

let+: destructuring extension of let*

This library implements the let+ macro, which is a dectructuring extension of let*.


  • clean, consistent syntax and small implementation (less than 300 LOC, not counting tests)
  • placeholder macros allow editor hints and syntax highlighting
  • &ign for ignored values (in forms where that makes sense)
  • very easy to extend

Similar libraries

This library was inspired by Gary King’s excellent metabang-bind. I have been using the latter for years now, but at some point I decided to write a library of my own, aiming for a cleaner syntax, more concise implementation and a more consistent interface (whether I have succeeded is of course a matter of judgement — try metabang-bind to see if you like it better).

In my opinion the main advantages of this library, compared to metabang-bind, are the placeholder macros which provide editor hints and the more consistent syntax of destructuring forms. In particular, when both read-write and read-only forms are available the latter always have the -r/o suffix, &flet and &labels resemble the Common Lisp syntax more closely, and the library should be easier to extend.

You can find other pattern matching libraries on cliki.


let+ ({binding}*) body*


binding ::= symbol || (form [init-form])

LET+ is recursive: each binding is in the scope of the previous ones. Forms ignore &ign variables (where applicable).

Built-in forms

Forms which provide both read-write and read-only access are available as &form and &form-r/o. The first one always uses symbol macros, so you can use setf. The second one reads the values at the beginning of the list from value: you can change these variables after that without having any effect on the original value. Read-only forms may also provide a slight increase in speed, and promote good style — you can use them to signal that you will not change the original structure.

The following forms are defined:

var, (var), (var value)
These behave just like they do in let*.
(list value)
When list is not recognized as any of the forms below, it is simply destructured using destructuring-bind. &ign are ignored. Example:
(let+ (((a (b &optional (c 3)) &ign &key (d 1 d?)) '(1 (2) 7 :d 4)))
  (list a b c d d?))  ; => (1 2 3 4 T)
((&slots slot*) value), also &slots-r/o
Similarly to with-slots, each slot has the syntax variable or (variable) (for these, the variable name is also used for the slot name) or (variable slot-name). &slots-r/o provides read-only bindings.


(defclass foo-class ()
  ((a :accessor a :initarg :a)
   (b :accessor b-accessor :initarg :b)))

(let+ (((&slots a (my-b b)) (make-instance 'foo-class :a 1 :b 2)))
  (list a my-b))  ; => (1 2)
((&accessors accessor*) value), also &accessors-r/o
Syntax similar to &slots, but uses accessors. Continuing the example above:
(let+ (((&accessors a (b b-accessor)) (make-instance 'foo-class :a 1 :b 2)))
  (list a b))  ; => (1 2)
((&structure conc-name slot*) value), also &structure-r/o
Slot access for structures. Conc-name is prepended to the accessors (you need to include the - if there is one). Example:
(defstruct foo-struct c d)
(let+ (((&structure foo-struct- c (my-d d)) (make-foo-struct :c 3 :d 4)))
  (list c my-d))  ; => (3 4)
((&values value*) form)
Similar to multiple-value-bind. &ign are ignored. Example:
(let+ (((&values a &ign b) (values 1 2 3)))
  (list a b))  ; => (1 3)
(array value) (only read-only version)
The array is destructured to the given elements, &ign are ignored. Indexes use row-major access, determined at macroexpansion time. Example:
(let+ ((#(a &ign b) (vector 1 2 3)))
  (list a b))  ; => (1 3)
((&array-elements (variable subscript*)*) value), also &array-elements-r/o
Array elements with given subscripts are assigned to the variables. Example:
(let+ (((&array-elements (a 0 1)
                         (b 2 0))
        #2A((0 1)
            (2 3)
            (4 5))))
  (list a b))  ; => (1 4)
((&flet name lambda-list forms*)), also &labels
Function bindings. These have no value form. &labels allows the function to refer to itself – note that since let+ is always recursive, this is the only difference between the two forms. Example:
(let+ (((&flet add2 (x)
          (+ x 2))))
  (add2 5))  ; => 7
((&plist (variable key [default])*), also &plist-r/o
Access to property lists. When key is not given, variable is used instead, and default is used if the element does not exist in the value (note that default may be evaluated multiple times when using the read-write form which uses symbol-macrolet). Example:
(let+ (((&plist a (my-b b) (c nil 3)) '(a 1 b 2)))
  (list a my-b c))  ; => (1 2 3)
(((&hash-table (variable key [default])*), also &hash-table-r/o
Access to the elements of hash tables, the semantics is the same as &plist.
(&complex real imaginary)
Destructures complex numbers.


You can nest let+ expressions when it makes sense (it doesn’t always, especially for read/write slots, the read only form should work). For example,

(let+ ((#((&complex a b)) (vector (complex 1 2))))
  (list a b))

should destructure the complex number that is the single element in the vector.

If you find that let+ does not nest properly, please report it as a bug.

Convenience macros

(defun+ name (argument*) form*), also (lambda (argument*) form*)
Work like defun and lambda, but arguments are destructured using let+. Example:
(defun+ foo ((&plist a b c) #(d e))
  (list a b c d e))

(foo '(a 1 b 2 c 3) #(4 5))  ; => (1 2 3 4 5)

See also &labels+ and &lambda+.

Can be used to provide destructuring forms for structures.

Other forms

  • (&once-only symbols ...) and (&with-gensyms symbols) are useful for writing macros.


Extending let-plus is very easy: if you want to use a form that resembles a list, you just have to define a method for let+-expansion-for-list. There is a macro that helps you with that, called define-let+-expansion. If the library didn’t have &complex, we could define destructuring for the form like this:

(define-let+-expansion (&complex (x y))
  "Access real and imaginary part of the value.  Read-only."
  `(let ((,x (realpart ,value))
         (,y (imagpart ,value)))

Some highlights:

  • this macro defines a “placeholder” macro &complex that should help with editor hints, but has no other purpose (it is not used in the expansion),
  • the macro is anaphoric, capturing value (the value form) and body (the body inside the let+ form), you can customize both of this using keyword arguments,
  • unless required otherwise, value is wrapped in once-only preventing multiple evaluations of the same form. See the arguments :uses-value? and :once-only? for define-let+-expansion.

If you want to extend let+ with forms that are not lists (eg like the array syntax above), have a look at let+-expansion.

Reporting bugs

Please open an issue on Github for bugs. Extensions are also welcome, either as forks or small code snippets submitted as issues. Wishlist items are also welcome!

I ask you not to report bugs via e-mail if you can avoid it. Tracking bugs on Github makes it less likely that they get lost.