3

I have an alist whose keys are strings, and I need to set the value for one of its keys. If the key is not present, I need to add it. So for example,

(alist-set "a" "bb" '(("a" . "b") ("c" . "d"))) => '(("a" . "bb") ("c" . "d"))
(alist-set "a" "bb" '(("c" . "d"))) => '(("a" . "bb") ("c" . "d"))

Introducing duplicate entries is not acceptable. Mutating the original alist is allowed.

Here are some incorrect solutions:

  • Using alist-get:

     (setf (alist-get key alist) val)
    

    This is incorrect because alist-get compares keys using eq, but string keys must be compared using equal. I could implement a new version of the setf accessor for alist-get that uses equal, but the original implementation in gv.el is 26 lines of code. That is a lot of boilerplate for this simple operation.

  • Using setcdr with assoc:

    (setcdr (assoc key alist) val)
    (setf (cdr (assoc key alist)) val)
    

    This is incorrect because it fails when the key is not present in the alist to begin with.

  • Use the fact that earlier entries in an alist shadow later ones:

    (push (cons key val) alist)
    

    This is incorrect because it introduces a duplicate entry for key in the alist.

Here are some questions whose answers do not answer this question:

  • One wonders why you are using an alist if "introducing duplicate entries is not acceptable". Why not use some other representation (e.g. a hash list)? Is it because you have no control over the source data and you don't want to create another representation for it? – Drew Jul 1 '17 at 15:57
  • Please consider filing an enhancement request for alist-get to accept an optional TEST parameter, which provides the comparison predicate to use. Alists with string keys are common. Seems like an oversight that alist-get hard-codes eq. – Drew Jul 1 '17 at 16:15
  • @Drew I am writing a package manager, and one of its functions is recording the commits of each package repository, and writing them to a lockfile. Writing them as an alist is pretty and makes for elegant diffs, as well as being human-readable/writable. Using a hash table would not be as nice. – Radon Rosborough Jul 1 '17 at 16:22
  • @Drew I would file an enhancement request, but the Emacs community does not currently make it easy to do so, and I do not currently have the time to figure out the process. If someone else wishes to, I think it's a great idea. – Radon Rosborough Jul 11 '17 at 19:47
  • 1
    It as simple as M-x report-emacs-bug RET. – politza Jul 17 '17 at 17:26
3

Since Emacs version 26.1, alist-get accepts an optional argument testfn; when this argument is non-nil, then alist-get uses it to compare the keys instead of eq.

Then you can do:

(let ((alist (list (cons "a" "b") (cons "c" "d"))))
  (setf (alist-get "a" alist nil nil 'equal) "bb")
  alist)
=> (("a" . "bb") ("c" . "d"))

Another way is to use the builtin library map.el. This library adds an abstration layer to handle uniformly hash-tables, alists and arrays. This has the advantage that if you later decide to use a hash-table instead of an alist, your code using map.el remains valid.

With map.el you'd write:

(require 'map)
(let ((map (list (cons "a" "b") (cons "c" "d"))))
  (map-put map "a" "bb" 'equal)
  map)
=> (("a" . "bb") ("c" . "d"))
  • This is great! I had never seen map.el before. It should be noted that this library is also recent, having been added in 25.1. endlessparentheses.com/new-in-emacs-25-1-map-el-library.html – Radon Rosborough Jul 17 '17 at 16:43
  • Plot twist: the TESTFN argument to map-put was only added in Emacs 26.1 as well. Thus, I'm unaccepting this answer until Emacs 26.1 comes out. – Radon Rosborough Aug 26 '17 at 2:28
  • 1
    @RadonRosborough, Emacs 26.1 is out ;-)! I think you may accept this answer now. – ben Jun 3 at 13:19
3

I came up with this solution, which does not require traversing the alist twice, and satisfies all of my other conditions.

(defun alist-set (key val alist &optional symbol)
  "Set property KEY to VAL in ALIST. Return new alist.
This creates the association if it is missing, and otherwise sets
the cdr of the first matching association in the list. It does
not create duplicate associations. By default, key comparison is
done with `equal'. However, if SYMBOL is non-nil, then `eq' is
used instead.

This method may mutate the original alist, but you still need to
use the return value of this method instead of the original
alist, to ensure correct results."
  (if-let ((pair (if symbol (assq key alist) (assoc key alist))))
      (setcdr pair val)
    (push (cons key val) alist))
  alist)

You will need to include

(require 'subr-x)

in order to have access to if-let.

  • 3
    You can use instead(eval-when-compile (require 'subr-x)) because if-let is a macro. – Tino Jul 1 '17 at 8:03

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.