Finding a missing form
with-slots inspires a
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-accessors for convenient access to CLOS objects, so why not a
with-plist for convenient access to plist values?
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:
;; looking at with-slots (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)))
The symbol macrolet replaces instances of the symbols
my-c with slot access forms within the body of
with-slots. We may profit from the same technique in the construction of
But first, an hypothetical example of
with-plist in use:
Consider the following example:
;; considering a hypothetical with-plist (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.
:|currentJob| above), then some kind of local name should be provided.
Otherwise it works just like
A Draft of the Macro
;; draft 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!