4

I'm curious if there's an equivalent for let-alist for plists. Namely, a macro for accessing plists in the manner that alists may be accessed as:

(let-alist alist
  (if (and .title .body)
      .body
    .site
    .site.contents))

It would probably be necessary to translate keyword symbols, of course, so a plist like the following may be accessed:

(my/let-plist '(:foo "bar" :baz "bat")
  (if (equal .foo "bar")
      .baz
    "default"))
4

It isn't difficult to make your own let-plist by referring to let-alist.el, here is my attempt, the mainly difference is let-plist--list-to-sexp.

(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)))
                     (delete-dups (let-alist--deep-dot-search body)))
         ,@body))))

Here are some uses.

(let-plist '(:foo 1 :bar 2)
  .bar)
;; => 2

(let-plist '(:question (:title
                        "Equivalent of let-alist for plists?"
                        :id
                        "45581"
                        :tags
                        ("property-lists")))
  .question.title)
;; => "Equivalent of let-alist for plists?"
3

You could use:

(pcase-defmacro plist (&rest args)
  `(and (pred listp)
        ,@(mapcar (lambda (elt)
                    (if (consp elt)
                        `(app (pcase--flip plist-get ,(car elt)) ,(cadr elt))
                      `(app (pcase--flip plist-get ',elt)
                            ,(if (keywordp elt)
                                 (intern (substring (symbol-name elt) 1))
                               elt))))
                  args)))

after which you can do

(pcase-let (((plist :key val) `(:key 4 val 5)))
  (+ key val))

or

(pcase-let (((plist (:key k) ('val v)) `(:key 4 val 5)))
  (+ k v))

The code was an adaptation of the map pcase pattern (which can be used for alists, hash-tables, and arrays).

  • 1
    Thanks; this is so cool! I frequently use the -let* destructuring sugar for alists/plists (-let* (((&plist :some-prop some-prop) my-plist)) ...) and the notion of custom/extensible destructuring never occurred to me! The syntax is a little verbose for my taste though (hence the question). – ebpa Oct 29 '18 at 14:52
  • @ebpa: Extensibility and generality come at a cost in terms of conciseness, indeed. – Stefan Oct 29 '18 at 18:34
2

The option that I have resorted to previously is converting the plist to an alist using the kv package (kvplist->alist):

(let-alist (kvplist->alist '(:foo "bar" :baz (:bat "blah"))
  .foo)

However, this only addresses the shallow case; it doesn't offer nested access via .baz.bat.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.