WITH-PLIST

#commonlisp

When I'm deserializing JSON, I usually end up with a property list, also called a plist (I pronounce it “Pea List”). But extracting values from plists using getf over and over again gets tedious.

Common Lisp already has with-slots and with-accessors for convenient access to CLOS objects, so why not a with-plist for convenient access to plist values?

What should with-plist do exactly? Let's look at with-slots for guidance.

WITH-SLOTS as a Guide

Observe the following:


;; define a class to test with-slots with 
(defclass abc ()
  ((a :initform 0)
   (b :initform 0)
   (c :initform 0)))

(let ((ob (make-instance 'abc))) 
  ;; bind slots by name or give a local-name to a slot binding
  (with-slots (a (my-b b) (my-c c)) ob 
    (setf a 10     
          my-b 20 
          my-c 30) 
    (format t "a=~a, my-b=~a, my-c=~a~%" 
            a my-b my-c)) 
  (format t "ob's b = ~a~%" (slot-value ob 'b)))

;; the above prints:
;; a=10, my-b=20, my-c=30
;; ob's b = 20

So here you see that with-slots allows the programmer to associate accessors with an object's slots, optionally giving those accessors a name that differs from correpsonding slot's name. Furthermore, when those variables are mutated with setf, the object itself is also mutated. How does with-slots accomplish this?

To find out, lets crack it open with macroexpand and see what's inside:


(macroexpand 
 '(with-slots (a (my-b b) (my-c c)) ob 
   (setf a 10 
    my-b 20 
    my-c 30) 
   (format t "the OB has a=~a, b=~a, c=~a~%" 
    a my-b my-c)))

;; returns

(LET ((#:G252 OB))
  (DECLARE (IGNORABLE #:G252))
  (DECLARE (SB-PCL::%VARIABLE-REBINDING #:G252 OB))
  (SYMBOL-MACROLET ((A (SLOT-VALUE #:G252 'A))
                    (MY-B (SLOT-VALUE #:G252 'B))
                    (MY-C (SLOT-VALUE #:G252 'C)))
    (SETF A 10
          MY-B 20
          MY-C 30)
    (FORMAT T "the OB has a=~a, b=~a, c=~a~%" A MY-B MY-C)))

Ah-hah! It's symbol-macrolet!

The symbol macrolet replaces instances of the symbols a, my-b and my-c with slot access forms within the body of with-slots. We may profit from the same technique in the construction of with-plist.

But first, an hypothetical example of with-plist in use:

Using WITH-PLIST

Consider the following example:


(let ((pl
        (list 'name "Buckaroo Banzai"
              :age 29
              :|currentJob| "Astro-Spy Rocker")))
  (with-plist (name (age :age) (job :|currentJob|)) pl 
    (incf age) 
    (format t "~a the ~a had a birthday and is now ~a years old~%" 
            name job age) 
    pl))
;; prints out
;; Buckaroo Banzai the Astro-Spy Rocker had a birthday and is now 30 years old

;; and returns
;; (NAME "Buckaroo Banzai" :AGE 30 :|currentJob| "Astro-Spy Rocker")

Here you can see that with-plists should be able to access keys of differing types, associate names for those accessors, and update the plist by referencing those names.

If the key is an ordinary symbol (e.g name above), then you can use that symbol itself to name the accessor. Otherwise, if the key is a keyword symbol (e.g. :age and :|currentJob| above), then some kind of local name should be provided.

Otherwise it works just like with-slots.

A Draft of the Macro


(defmacro with-plist (keys plist &body body)
  (let* ((plist-var
           (gensym))
         (macrolet-bindings
           (loop for term in keys
                 when (consp term)  
                   collect (destructuring-bind (var key) term
                             `(,var (getf ,plist-var ',key)))
                 else
                   collect `(,term (getf ,plist-var ',term)))))
    `(let ((,plist-var ,plist))
       (symbol-macrolet ,macrolet-bindings ,@body))))

The macro first determines the names of the symbol-macrolet bindings before binding each one to a getf form that accesses the plist. Thats it!