2

add-to-list doesn't refresh item, it only checks for existence of item by equal or custom comparison function:

(add-to-list
 'tramp-methods
 '("gssh" (tramp-login-program "gcloud compute ssh"))
 nil (lambda (a b) (equal (car a) (car b))))

What way can I replace definition in a list, that handles presence/absence of item and support custom comparison function?

tramp-methods is an association list. Is there something to set key/value with replacing existing entry?

UPDATE I found cl-pushnew & cl-adjoin but they don't replace, only adds if not there...

UPDATE 2 Found exactly same question: https://stackoverflow.com/questions/10063195/replace-item-in-association-list-in-elisp

There is no build-in library function that handles replacement of existing items with custom key/test function & that handles missing item case...

The close solution:

(setq tramp-methods (cons
                     '("gssh" (tramp-login-program "compute ssh 2"))
                     (cl-remove "gssh" tramp-methods :key 'car :test 'equal)))

I wonder if there is some cl- equivalent...

UPDATE 3 cl-union is the most closed so far, but it has undefined behavior when elements are equal...

Drew
  • 75,699
  • 9
  • 109
  • 225
gavenkoa
  • 3,352
  • 19
  • 36

3 Answers3

2

You can use alist-get with setf (elisp) Generalized Variables, e.g.,

;; Add
(let ((al (list (cons 'a 1) (cons 'b 2))))
  (setf (alist-get 'c al) 3)
  al)
;; => ((c . 3) (a . 1) (b . 2))

;; Replace/update
(let ((al (list (cons 'a 1) (cons 'b 2))))
  (setf (alist-get 'b al) "2")
  al)
;; => ((a . 1) (b . "2"))
(add-to-list
 'tramp-methods
 '("gssh" (tramp-login-program "gcloud compute ssh"))
 nil (lambda (a b) (equal (car a) (car b))))

(setf (alist-get "gssh" tramp-methods nil nil #'equal)
      '(tramp-login-program "GCLOUD COMPUTE SSH"))

(car tramp-methods)
;; => ("gssh" tramp-login-program "GCLOUD COMPUTE SSH")

alist-get was added in Emacs 25.1, its optional argument TESTFN was added in Emacs 26.1.

xuchunyang
  • 14,302
  • 1
  • 18
  • 39
  • I believe your code doesn't handle the case when there is no key in the alist. Regarding `setf` - nice to know! Checks for key existence asks for some utility method/macro (to avoid duplication). – gavenkoa Nov 01 '20 at 14:45
  • 1
    If the key doesn't exist in the alist, then the setf will cause it to be added – rpluim Nov 01 '20 at 14:59
  • 1
    @gavenkoa in the first example, the key `c` does not exists, `setf` adds the key/value successfully. – xuchunyang Nov 01 '20 at 15:49
  • Yeah, it works. Now I need to grasp `alist-get` + `setf`. Seems that `setf` macro has special support for `alist-get`. – gavenkoa Nov 01 '20 at 17:39
  • 1
    @gavenkoa yeah, `setf` is interesing, e.g., `(setf (point) (point-min))` expands into `(goto-char (point-min))` – xuchunyang Nov 01 '20 at 17:53
  • I started to use `setf` and got a problem with duplication when strings are used: https://emacs.stackexchange.com/questions/61668/setf-alist-get-but-with-equal-instead-of-eq – gavenkoa Nov 10 '20 at 12:02
1

I ended with function like:

(defun my-assoc-push (key value alist-name)
  (when (not (symbolp alist-name)) (error "alist-name is not a symbol."))
  (set alist-name
       (cons (cons key value)
             (cl-remove key (symbol-value alist-name) :key #'car :test #'equal))))

(my-assoc-push "gssh"  '((tramp-login-program "compute ssh 3")) 'tramp-methods)

to replace/add if new key to an association list.

gavenkoa
  • 3,352
  • 19
  • 36
  • 1
    I would rename the `alist` argument to `alist-name` in order to avoid the cognitive dissonance of "alist is not a symbol" - my reaction to that would be: "Of course, it's not a symbol - it's an alist" :-) – NickD Nov 01 '20 at 13:14
1

tramp-methods is an association list.

Which means that you don't have to replace an existing value at all.

When a value is looked up in an alist, only the first match for the key is returned.

Therefore merely pushing a new (KEY . VALUE) onto the front of the list has the desired effect, regardless of whether or not there are other uses of that same KEY in the list already.

phils
  • 48,657
  • 3
  • 76
  • 115
  • During debugging I polluted alist with same keys. Will reloading of init files pollute alists too (with `push`, `add-to-list` tries to match based on `equals` at least...)? – gavenkoa Nov 01 '20 at 22:12
  • 1
    It all depends on what you want to do, really. `add-to-list` won't change anything if the specified key.value already exists in the list, but if it *does* exist but isn't the *first* instance of that key, then you also won't be getting that value back from a subsequent query. Conversely if you push then you'll definitely get the pushed value back from a subsequent query, but you *might* be pushing a redundant duplicate. – phils Nov 01 '20 at 22:55