Iterate over plist and set value per key

202 Views Asked by At

How can I iterate over a plist, prompt for each key a value and fill in said value into the plist?

I have 'the skeleton' of a list of properties for my projects

(defconst project-properties
  '(:number nil
    :place nil
    :location nil))

I copy this list and fill in the values so I keep a skeleton list and have the filled in list to process it further.

I managed to do it for an alist, but am unable to change the functionality to let it work with plists.

(defun project-prompt-properties-alist ()
  "Prompt for project properties and return them"
  (let ((properties (copy-alist project-properties)))
    (cl-loop for (prop . val) in properties
             do (setf (alist-get prop properties) (read-string (format "Geef %s: " prop))))
    properties))
2

There are 2 best solutions below

1
ad absurdum On BEST ANSWER

Both Common Lisp and Elisp allow you to loop over a list using on which iterates on successive tails of the list, instead of in which iterates on successive elements of the list. For example:

CL-USER> (loop for tail on project-properties
               do (format t "~A~%" tail))
(NUMBER NIL PLACE NIL LOCATION NIL)
(NIL PLACE NIL LOCATION NIL)
(PLACE NIL LOCATION NIL)
(NIL LOCATION NIL)
(LOCATION NIL)
(NIL)

You can further use the by keyword to reduce the input list by two elements instead of one on each iteration:

CL-USER> (loop for tail on project-properties by #'cddr
               do (format t "~A~%" tail))
(NUMBER NIL PLACE NIL LOCATION NIL)
(PLACE NIL LOCATION NIL)
(LOCATION NIL)

You can use destructuring to take the first two elements of each tail:

CL-USER> (loop for (prop val) on project-properties by #'cddr
               do (format t "~A -> ~A~%" prop val))
NUMBER -> NIL
PLACE -> NIL
LOCATION -> NIL

These features work the same in Elisp. You should be able to write project-prompt-properties-plist for Elisp like this:

(defun project-prompt-properties-plist ()
  "Prompt for project properties and return them"
  (let ((properties (copy-sequence project-properties)))
    (cl-loop for (prop val) on properties by #'cddr
             do (plist-put properties prop (read-string (format "Geef %s: " prop))))
    properties))
0
Svante On

If you want to collect user input for a list of properties, you can use that list directly to build the plist:

(loop :for property :in '(:number :place :location)
      :collect property
      :collect (read-query-whatever property))

or the alist:

(loop :for property :in '(:number :place :location)
      :collect (cons property
                     (read-query-whatever property)))

If you want to update properties that the user might want to leave unchanged, then you can also go over the given plist as you showed with the skeleton idea:

(loop :for (property value) :on '(:number 23 :place nil :location "moon") :by #'cddr
      :collect property
      :collect (read-query-whatever property value))

or the same with an alist:

(loop :for (property . value) :in '((:number . 23) (:place . nil) (:location "moon"))
      :collect (cons property
                     (read-query-whatever property value)))

These all build a new list step by step, instead of first copying and then updating it.