4

Suppose I have a list variable *test* set to (:v1 (1) :v2 (2))

And then, after some string paring, I will need to add another 1 to :v1 , something equivalent to:

(push 1 (getf *test* :v1))

However, it will look more like:

(push 1 (getf-string-equal *test* "v1"))

Where the getf-string-equal is (taken from here)

(defun getf-string-equal (plist indicator)
  (loop
     for (i v) on plist by #'cddr
     when (string-equal i indicator)
     return v))

My problem, however, is that I cannot use setf on the returned list. I can use some ugly tricks to push inside the function, by side effects, but I am trying to avoid that.

How can I modify a list property which I obtained by searching for the property as a string? Something equivalent to:

(push 1 (getf-string-equal *test* "v1"))

Thanks.

Makketronix
  • 1,389
  • 1
  • 11
  • 31

1 Answers1

3

Note: This answer is not a complete solution, as it does not handle the case where the key is not found. It is simply to point you in the right direction.

There are a couple of layers to this question. First off, push is a macro that more or less expands to a setf call. Thus, we need to define a setf expansion for getf-string-equal. That's fairly easy to do with one little complication.

(defsetf getf-string-equal (plist indicator) (value)
  (let ((i (gensym))
        (rest (gensym))
        (match (gensym)))
    `(let ((,match (loop
                      for (,i . ,rest) on ,plist by #'cddr
                      when (string-equal ,i ,indicator)
                      return ,rest)))
       (if ,match
           (setf (car ,match) ,value)
           nil))))

Let's break that down. We're using the long form of defsetf to define an expansion for our function. plist and indicator are the arguments, same as before, and value is a new value to assign. But this isn't a function; it's closer to a macro. So we're going to generate some code. To that end, we need some gensym calls to get some temporary variables.

Next, we generate code that runs the same loop that you used in your accessor. The difference here is that, rather than returning the actual value, we're going to return the cons cell that encloses it. That way, we can set the car in that cell and have the effect actually modify the data structure. If the match is found, we do just that, and we're done.

However, the problem arises if the match isn't found. If there is no such cons cell, we can't possibly set its car. That's the second case in this if. We'd like to simply expand to a `(setf ,plist ,value), which would have the correct semantics. However, defsetf won't let us have access to the actual variable used like a macro would. Thus, to fully solve this problem, including the corner case where the value is not found, we would have to dip into define-setf-expander, the completely general form of defsetf. To give you a taste of the generality of this macro, to correctly write a setf expansion using it, your expansion must be an expression which returns five different values.

Silvio Mayolo
  • 62,821
  • 6
  • 74
  • 116
  • 1
    Nice trick. Although I ended up using the trick in Sylwester comment, I am accepting this because it shows how to solve the problem, which in turns explains how to solve a class of similar problems. – Makketronix Jan 10 '18 at 23:45
  • Yeah, if you can avoid the black hole that is `setf` expansion, I highly recommend doing so. I think that, every time I've had to do a `setf` expansion, I've had to look up the `defsetf` macro and remind myself of the way its arguments work. Hopefully this answer will help some other people will similar issues. :) – Silvio Mayolo Jan 10 '18 at 23:48