let-plus icon indicating copy to clipboard operation
let-plus copied to clipboard

destructuring extension of let*

#+TITLE: =let+=: destructuring extension of =let*= #+AUTHOR: Tamás K. Papp

IMPORTANT This library is [[https://tpapp.github.io/post/orphaned-lisp-libraries/][unmaintained]].

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

  • Highlights
  • 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 [[http://common-lisp.net/project/metabang-bind/][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 [[http://common-lisp.net/project/metabang-bind/][metabang-bind]] to see if you like it better).

In my opinion the main advantages of this library, compared to [[http://common-lisp.net/project/metabang-bind/][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 [[http://www.cliki.net/pattern%20matching][cliki]].

  • Syntax

#+BEGIN_SRC lisp let+ ({binding}) body #+END_SRC where #+BEGIN_SRC lisp binding ::= symbol || (form [init-form]) #+END_SRC

=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: #+BEGIN_SRC lisp (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) #+END_SRC

  • =((&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.

    Example: #+BEGIN_SRC lisp (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) #+END_SRC

  • =((&accessors accessor*) value)=, also =&accessors-r/o= :: Syntax similar to =&slots=, but uses accessors. Continuing the example above: #+BEGIN_SRC lisp (let+ (((&accessors a (b b-accessor)) (make-instance 'foo-class :a 1 :b 2))) (list a b)) ; => (1 2) #+END_SRC

  • =((&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: #+BEGIN_SRC lisp (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) #+END_SRC

  • =((&values value*) form)= :: Similar to =multiple-value-bind=. =&ign= are ignored. Example: #+BEGIN_SRC lisp (let+ (((&values a &ign b) (values 1 2 3))) (list a b)) ; => (1 3) #+END_SRC

  • =(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: #+BEGIN_SRC lisp (let+ ((#(a &ign b) (vector 1 2 3))) (list a b)) ; => (1 3) #+END_SRC

  • =((&array-elements (variable subscript*)*) value)=, also =&array-elements-r/o= :: Array elements with given subscripts are assigned to the variables. Example: #+BEGIN_SRC lisp (let+ (((&array-elements (a 0 1) (b 2 0)) #2A((0 1) (2 3) (4 5)))) (list a b)) ; => (1 4) #+END_SRC

  • =((&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: #+BEGIN_SRC lisp (let+ (((&flet add2 (x) (+ x 2)))) (add2 5)) ; => 7 #+END_SRC

  • =((&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: #+BEGIN_SRC lisp (let+ (((&plist a (my-b b) (c nil 3)) '(a 1 b 2))) (list a my-b c)) ; => (1 2 3) #+END_SRC

  • =(((&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.

** Nesting

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, #+BEGIN_SRC lisp (let+ ((#((&complex a b)) (vector (complex 1 2)))) (list a b)) #+END_SRC 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: #+BEGIN_SRC lisp (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) #+END_SRC See also =&labels+= and =&lambda+=.

  • =define-structure-let+= :: Can be used to provide destructuring forms for structures.

** Other forms

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

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:

#+BEGIN_SRC lisp (define-let+-expansion (&complex (x y)) "Access real and imaginary part of the value. Read-only." `(let ((,x (realpart ,value)) (,y (imagpart ,value))) ,@body)) #+END_SRC 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 [[https://github.com/tpapp/let-plus/issues][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.