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!