Manfred
Manfred

Reputation: 517

Get only the keys of a plist

I do the following code to retrieve only the keys of a plist:

(loop :for (key nil) :on config :by #'cddr
      :collect key))

Running this produces:

CONFIG-TEST> (loop :for (key nil) :on '(:foo 1 :bar 2) :by #'cddr
                   :collect key)
(:FOO :BAR)

Is there a more 'functional' way to do this than using LOOP?

Upvotes: 3

Views: 1395

Answers (7)

oyvinht
oyvinht

Reputation: 11

(defun plist-keys (plist)
  "Get just the keys of a plist."
  (loop for key in plist by #'cddr collecting key))

(defun plist-values (plist)
  "Get just the values of a plist."
  (loop for value in (rest plist) by #'cddr collecting value))

Upvotes: 1

Ehvince
Ehvince

Reputation: 18395

With the Serapeum library, which I consider as a second must-have just after Alexandria: use plist-keys :)

(serapeum:plist-keys '(:a 1 :b 2))
;; (:A :B)

https://github.com/ruricolist/serapeum/blob/master/REFERENCE.md#plist-keys-plist

Here's its implementation:

(defun plist-keys (plist)
  "Return the keys of a plist."
  (collecting*
    (doplist (k v plist)
      (collect k))))

It also has plist-values.

Upvotes: 3

Gwang-Jin Kim
Gwang-Jin Kim

Reputation: 9965

(ql:quickload :alexandria)

(mapcar #'car (alexandria:plist-alist '(:a 1 :b 2)))
;; => (:A :B)

To remove dependency of alexandria, define yourself plist-alist:

(defun plist-alist (l &optional (acc '()))
  (cond ((null l) (nreverse acc))
        (t (plist-alist (cddr l) (cons (cons (car l) (cadr l)) acc)))))

However, dependency on :alexandria should not be counted as dependency.

directly

Actyally, one could change plist-alist definition to obtain only the keys:

(defun plist-keys (l &optional (acc '()))
  (cond ((null l) (nreverse acc))
        (t (plist-keys (cddr l) (cons (car l) acc)))))

And likewise the values:

(defun plist-vals (l &optional (acc '()))
  (cond ((null l) (nreverse acc))
        (t (plist-vals (cddr l) (cons (cadr l) acc)))))

Upvotes: 1

coredump
coredump

Reputation: 38924

Using the SERIES package, scan-plist returns two series, one for the keys, the other for values:

(scan-plist '(:a 3 :b 2))
=> #Z(:A :B)
   #Z(3 2)

You can rely on this to collect the first series as a list:

(collect 'list (scan-plist '(:a 3 :b 2)))

More generally, you may want to process the values in some way, so you would use mapping. For example, here is a plist-alist made with SERIES:

(defun plist-alist (plist)
  (collect 'list
    (mapping (((k v) (scan-plist plist))) (cons k v))))

Upvotes: 5

Gwang-Jin Kim
Gwang-Jin Kim

Reputation: 9965

If you are sure that none of the values are of type symbol, you could filter for symbols:

(remove-if-not #'symbolp '(:a 1 :b 2)) ;;=> (:A :B)

Much less efficient, but universersal:

  • filter for symbolp and getf-ability (Only keys of a plist are getf-able from the plist, thus this is the check whether it is a key or not. However, a check, whether an element in a plist is symbolp is cheaper and removes most of the non-key values, thus saving time and cost).
(defun get-plist-keys (plist)
  (remove-if-not #'(lambda (x) (and (symbolp x) (getf plist x))) plist))

(get-plist-keys '(:a 1 :b 2 :c :d))
;; => (:A :B :C)

Upvotes: 2

chrnybo
chrnybo

Reputation: 145

What stylistic direction would do take us?

CL-USER> (do ((result (list) (cons (car plist) result))
              (plist '(:foo 1 :bar 2) (cddr plist)))
             ((null plist) (reverse result)))
(:FOO :BAR)

By the way, I'd write the loop with less syntax, will this bite me?

CL-USER> (loop for key in '(:foo 1 :bar 2) by 'cddr
               collecting key)
(:FOO :BAR)

Upvotes: 3

Rainer Joswig
Rainer Joswig

Reputation: 139381

Not really...

CL-USER 35 > (let ((? nil))
               (mapcon (lambda (l)
                         (when (setf ? (not ?))
                           (list (first l))))
                       '(:foo 1 :bar 2)))
(:FOO :BAR)

or maybe:

(defun mapncar (fn list &key (start 0) (n 1))
  (loop for l = (nthcdr start list) then (nthcdr n l)
        while l
        collect (funcall fn (first l))))

CL-USER 61 > (mapncar #'identity '(a 1 b 2 c 3) :n 2)
(A B C)

CL-USER 62 > (mapncar #'identity '(a 1 b 2 c 3) :start 1 :n 2)
(1 2 3)

Upvotes: 4

Related Questions