p-list Destructuring

I'm dealing with plists (lists of a form '(:key1 "val1" :key2 "val2" ...)) in org-mode or in hella-sounds.el. And I wanted a way to bind variables to key values.

The problem is the repitition:

(let* ((date (plist-get info :date))
       (title (plist-get info :title))
       (title_html (plist-get info :title_html))
       (author (plist-get info :author))
       ;; ...
       )
  ;;finally do a thing
  )

Seems like we could shorten it.

So today I wrote a macro, plet:

(defmacro plet (plist keys &rest body)
  "Given a PLIST, bind symbols in KEYS to variables and do BODY.
Ex:
(let ((prop-l '(:key1 1 :key2 2 :key3 3)))
  (plet prop-l (key1 key2 key3 key4)
    (list key1 key2 key3)))
=> '(1 2 3 nil)
"
  (declare (indent 1))
  `(let ,(cl-loop for key in keys
                  collect (list key
                                ;; turn key1 -> :key1
                                `(plist-get
                                  ,plist
                                  ,(intern
                                    (concat ":" (symbol-name key))))))
     ,@body))

and I'll just run the code for you:

(let ((prop-l '(:key1 1 :key2 2 :key3 3)))
  (plet prop-l (key1 key2 key3 key4)
    (list key1 key2 key3)))
(1 2 3)

and this is how I did macro expansion:

(macroexp--expand-all '(let ((prop-l '(:key1 1 :key2 2 :key3 3)))
    (plet prop-l (key1 key2 key3 key4)
      (list key1 key2 key3))))

and I formatted the output for readability:

(let ((prop-l '(:key1 1 :key2 2 :key3 3)))
  (let ((key1 (plist-get prop-l :key1))
        (key2 (plist-get prop-l :key2))
        (key3 (plist-get prop-l :key3))
        (key4 (plist-get prop-l :key4)))
    (list key1 key2 key3)))

and it works. Unfortunately only after searching online, writing this macro, and debugging it; did I find other, inspirational answers online.

Before jumping in, I'll note that I have a requirement that if I access keys that don't exist, I don't want an error–I expect nil, as if I had just done a simple (plist-get plist :key).

Further, an ergonomic drawback of plet is that I'm declaring a keys list to bind to up front. It feels like duplication.

Here's are the other approaches I looked at:

cl-destructuring-bind

Stefan Monnier (link) suggested to fommil

(cl-destructuring-bind 
    (&key a b &allow-other-keys) 
    '(:a "foo" :b 13 :c "bar") 
  (list a b))

And this is great for fommil's question. But if I wanted to access a symbol, say that didn't exist in the &key specification, here just a b, of course it won't work because d is unbound.

(cl-destructuring-bind 
    (&key a b &allow-other-keys) 
    '(:a "foo" :b 13 :c "bar") 
  (list a b d))
let*: Symbols value as variable is void: d

Macro expanding like so

(macroexp--expand-all '(cl-destructuring-bind 
                       (&key a b &allow-other-keys) 
                       '(:a "foo" :b 13 :c "bar") 
                     (list a b d)))

gives

(let* ((--cl-rest-- '(:a "foo" :b 13 :c "bar"))
       (a (car (cdr (plist-member --cl-rest-- ':a))))
       (b (car (cdr (plist-member --cl-rest-- ':b)))))
  (list a b d))

cl-destructuring-bind again

fommil then updated his answer, and the call for plist-bind is more succinct, more readable, less extensible. It still has the same drawbacks however.

(defmacro plist-bind (args expr &rest body)
  "`destructuring-bind' without the boilerplate for plists."
  `(cl-destructuring-bind
       (&key ,@args &allow-other-keys)
       ,expr
     ,@body))

In action:

(let ((plist '(:a "foo" :b 13 :c "bar")))
  (plist-bind (a b) plist
              (list a b)))
("foo" 13)

But what I'm really look for is some way to destructure, without having to explicitly declare the items.

with-plist-vals

From John Kitchin's post, (I replaced loop with cl-loop since loop was removed):

(defmacro with-plist-vals (plist &rest body)
  "Bind the values of a plist to variables with the name of the keys."
  (declare (indent 1))
  `(let ,(cl-loop for key in (-slice plist 0 nil 2)
                  for val in (-slice plist 1 nil 2)
                  collect (list (intern
                                 (substring (symbol-name key) 1))
                                val))
     ,@body))
(with-plist-vals (:a 4 :b 6)
  (* 2 a)) ; => 8

This works in his example because he uses a literal list passed in as plist, but doesn't work we pass in a symbol referring to a list. Here's a clarifying example:

(let ((plist '(:a 4 :b 6)))
  (with-plist-vals 
      (* 2 a))) ; => nil

when we macro expand like this:

(macroexp--expand-all '(let ((plist '(:a 4 :b 6)))
                         (with-plist-vals 
                             (* 2 a))))

we get

(let ((plist '(:a 4 :b 6)))
  (let ((## 2)) nil))

The binding in the inner let is missing because (-slice plist 0 nil 2) is working on the symbol 'plist (an error), not the value (:a 4 :b 6).

That's ok. That can be fixed. What I like about it: it interns all the keys, so you don't have to explicitly declare which keys you want in another form like the previous solutions we've seen.

It still has the draw back that you can't access a key that isn't present.

(with-plist-vals (:a 4 :b 6)
  (* 2 a d)) ; => leads to Lisp error: (void-variable d)

So this leads me to think that a better design involves specially marking your variables, specifically for destructuring. We see that next.

let-plist

Someone asked about an impletmentation for let-plist, as an anologous utility to Mr.Endlessparentheses's let-alist. (link).

let-alist allows nested searching as well, which is more power than I was looking for. (There are other good responses in the post, and I'm particularly interested in Stefan's answer, but I don't understand pcase yet.)

From xuchunyang (who says the code they wrote isn't difficult to make, I don't know, it would have taken me a fair amount of time):

(require 'let-alist)

(defun let-plist--list-to-sexp (list var)
  "Turn symbols LIST into recursive calls to `plist-get' on VAR."
  `(plist-get ,(if (cdr list)
                   (let-plist--list-to-sexp (cdr list) var)
                   var)
              ',(intern (concat ":" (symbol-name (car list))))))

(defun let-plist--access-sexp (symbol variable)
  "Return a sexp used to access SYMBOL inside VARIABLE."
  (let* ((clean (let-alist--remove-dot symbol))
         (name (symbol-name clean)))
    (if (string-match "\\`\\." name)
        clean
        (let-plist--list-to-sexp
         (mapcar #'intern (nreverse (split-string name "\\.")))
         variable))))

(defmacro let-plist (plist &rest body)
  (declare (indent 1))
  (let ((var (make-symbol "plist")))
    `(let ((,var ,plist))
       (let ,(mapcar (lambda (x) `(,(car x) ,(let-plist--access-sexp (car x) var)))
                     ;; ((\.question.id . question.id) (\.question.author . question.author) (\.dinosaur . dinosaur))
                     (delete-dups (let-alist--deep-dot-search body)))
         ,@body))))

(let-plist plist
  (list .question.id .question.author .dinosaur))

And

(macroexp--expand-all '(let ((plist '(:question (:title
                                                 "Equivalent of let-alist for plists?"
                                                 :id
                                                 "45581"
                                                 :tags
                                                 ("property-lists")))))
                         (let-plist plist
                           (list .question.id .question.author .dinosaur))))

Leads to:

(let ((plist '(:question (:title "Equivalent of let-alist for plists?"
                                 :id "45581"
                                 :tags ("property-lists")))))
  (let ((plist plist))
    (let ((\.question.id (plist-get
                          (plist-get plist ':question)
                          ':id))
          (\.question.author (plist-get
                              (plist-get plist ':question)
                              ':author))
          (\.dinosaur (plist-get plist ':dinosaur)))
      (list
       \.question.id
       \.question.author
       \.dinosaur))))

And evaluates to

("45581" nil nil)

let-plist essentially does depth first search IN THE BODY for all symbols that start with ..

Then let-plist--access-sexp builds plist-get access chain for the let like (\.dinosaur (plist-get plist ':dinosaur)).

Be wary of nesting calls to let-plist1.

And finally, what is that sharp quote doing there? "\\`" will be read as \`, which is used to match the beginning of a string or buffer. I don't know why they didn't use ^ which is more understandable in my opinion: "^\\.", but this matches lines.

Conclusion

Many possibilities exist. I will probably stick with cl-destructuring-bind since it's built in, but it was a pretty fun exploration of macros which I don't do very often.

If you liked this synthesis, please econsider supporting this blog. It's because of supporters that I can dedicate my time and energy into exploring topics like this one and share them with others.

From 13 Jan 4XXX, have a nice day in 13 Jan 2XXX!


1

One issue I've had with let-alist is when an inner let-alist tries to access properties from the outer let-alist form. The outer let-alist won't read the inner let-alist form to expand accessors. Meaning if the inner form accesses a .property.from.outside, it will be nil.

I don't know how to get around that, or if I should.