6

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:

Tino
  • 420
  • 5
  • 9
Resigned June 2023
  • 1,502
  • 15
  • 20
  • 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 01 '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 01 '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. – Resigned June 2023 Jul 01 '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. – Resigned June 2023 Jul 11 '17 at 19:47
  • 1
    It as simple as `M-x report-emacs-bug RET`. – politza Jul 17 '17 at 17:26
  • @politza You're right, of course. I was thinking of emacs-devel, which is not nearly as pleasant. The point is moot, however, since apparently the functionality has already been added in Emacs trunk: https://emacs.stackexchange.com/a/34227/12534 – Resigned June 2023 Jul 17 '17 at 18:59
  • Yes, arguing its necessity would have been a whole different thing. – politza Jul 17 '17 at 19:15

2 Answers2

7

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"))
Resigned June 2023
  • 1,502
  • 15
  • 20
Tino
  • 420
  • 5
  • 9
  • 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. http://endlessparentheses.com/new-in-emacs-25-1-map-el-library.html – Resigned June 2023 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. – Resigned June 2023 Aug 26 '17 at 02:28
  • 1
    @RadonRosborough, Emacs 26.1 is out ;-)! I think you may accept this answer now. – ben Jun 03 '19 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.

Resigned June 2023
  • 1,502
  • 15
  • 20
  • 3
    You can use instead`(eval-when-compile (require 'subr-x))` because **if-let** is a macro. – Tino Jul 01 '17 at 08:03