16

Q: what is the idiomatic way to map a function across a property list?

The various mapping functions (mapcar and family) map a function over a sequence such as a list. How does one use these functions when dealing with a property list, i.e., when trying to map across each of the properties contained in the list (which would be every other element starting from the first one)? It seems to me that the mapping function would need to access the list in pairs of elements rather than as individual elements.

As a toy example, how would one take a property list and collect all of the property values? If it were an association list instead it would be pretty simple:

(mapcar #'cadr '((:prop1 a) (:prop2 b) (:prop3 c))) ;=> (a b c)

I'm sure this could be done with a loop, but it seems a little laborious and I'm wondering if there's a more idiomatic way to do it.

  • Please clarify whether you want to map over only the property values (which is what it sounds like, and which is what your mapcar alist example does) or you want to map over the pairs of property symbol and property value. The latter is more general (more generally useful), I would guess. – Drew Apr 10 '15 at 21:38
  • @Drew: I'm more interested in the general case; the example was the simplest one I could think of. I'd like to know how to map over the property/value pair. If the answer is "a loop," then so be it, but I'm wondering if there's a more elegant solution. – Dan Apr 10 '15 at 21:54
  • What you have in your example isn't a property list. Property lists have even number of elements, odd elements are property names, even elements are property values. What you have would be, probably, called a tree (a list of lists). – wvxvw Apr 11 '15 at 8:54
8

You will likely get various loop and iteration answers. AFAIK, there is no idiomatic way to do this. I don't even think it's very common to want to accumulate only the property values (not associating them with the properties).

Here is one simple way to do it:

(defun prop-values (plist)
  "..."
  (let ((pl    (cdr plist))
        (vals  ()))
    (while pl
      (push (car pl) vals)
      (setq pl  (cddr pl)))
    (nreverse vals)))

For the more general case, where you want to map a binary function over a plist:

(defun map-plist (fn plist)
  "..."
  (let ((pl    plist)
        (vals  ()))
    (while pl
      (push (funcall fn (car pl) (cadr pl)) vals)
      (setq pl (cddr pl)))
    (nreverse vals)))

(setq foo '(a 1 b 2 c 3 d 4 e 5))

(map-plist #'cons foo) ; => ((a . 1) (b . 2) (c . 3) (d . 4) (e . 5))
12

This would probably depend on a situation. In general, if I need to tie a number of values with a number of names, I'd use a hash-table, but if I have to use a property list, I'd use cl-loop. Below are some examples:

(cl-loop for (key value) on '(:prop1 a :prop2 b :prop3 c) by 'cddr
         collect value)
;;; (a b c)

And if you have a data-structure you show in your example:

(cl-loop for (key value) in '((:prop1 a) (:prop2 b) (:prop3 c))
         collect value)
;;; (a b c)
4

To me the answer is to turn the plist into an alist, which is an equivalent data-structure, just half as deep and easier to manipulate.

  • 3
    Well, that's kind of an answer (and good advice), but more of a comment. – Drew Apr 10 '15 at 21:54
4

With Emacs from the current Git HEAD which will become Emacs 25, you can do the following, i.e., turn the plist into an alist easily with the new seq-partition function, and then process the alist entries using standard mapcar.

(let* ((my-plist (list :a 1 :b 2 :c 3 :more (list 4 5 6)))
       (my-alist (seq-partition my-plist 2))
       (my-reverse-alist (mapcar (lambda (entry)
                                   (let ((prop (car entry))
                                         (val  (cadr entry)))
                                     (list val prop)))
                                 my-alist)))
  (message "my-plist: %s\nmy-alist: %s\nmy-reverse-alist: %s"
           my-plist my-alist my-reverse-alist))
;; my-plist: (:a 1 :b 2 :c 3 :more (4 5 6))
;; my-alist: ((:a 1) (:b 2) (:c 3) (:more (4 5 6)))
;; my-reverse-alist: ((1 :a) (2 :b) (3 :c) ((4 5 6) :more))

Have a look at the seq-partition docs using C-h f seq-partition RET.

1

One idea is to use -map-indexed from dash and apply transformation only to odd values of the list:

(-non-nil (-map-indexed (lambda (index item) (when (oddp index) item))
  '(a x b y c z))) ; => (x y z)
(-non-nil (--map-indexed (when (oddp it-index) it) '(a x b y c z))) ; => (x y z)

Another idea is to just convert a plist to a hash-table, using ht<-plist from ht:

(ht-values (ht<-plist '(a x b y c z) 'equal)) ; (z y x)

Notice that hash-table doesn't preserve the order of items.

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.