10

Suppose I've got an assoc-list like this:

(setq x '((foo . ((bar . "llama")
                  (baz . "monkey")))))

And I want the value at bar. I can do this:

(assoc-default 'bar (assoc-default 'foo x))

But what I'd really like is something that accepts multiple keys, like

(assoc-multi-key 'foo 'bar x)

Does such a thing exist, perhaps in a package somewhere? I'm sure I could write it, but I feel like my Google-fu is just failing and I can't find it.

  • FWIW, I don't see any nested alists on this page. I see only ordinary, unnested alists. And it is not clear what behavior you are looking for. You say nothing about the behavior of assoc-multi-key. Presumably it looks for matches to both of its first two arguments, but that's really all that one could suppose, from what you've said. And it clearly cannot accept more than two keys, since the alist argument (presumably x) is the last one, not the first one - which suggests that it is not too useful in general. Try actually specifying what you are looking for. – Drew Nov 6 '14 at 22:40
  • I also found the original formatting of the setq form in the example confusing, so I edited it to use the common dot-notation for assoc-lists. – paprika Nov 7 '14 at 0:46
  • Ah, OK. So the alist does have two levels. The question still is unclear - assoc-multi-key remains unspecified. – Drew Nov 7 '14 at 1:11
  • 1
    Drew: The point of assoc-multi-key is to look up the first key in the assoc-list. This should resolve to a new assoc-list in which we look up the next key. And so forth. Basically a short-hand for digging values out of nested assoc-lists. – abingham Nov 7 '14 at 5:51
  • 2
    @Malabarba Perhaps you could mention let-alist too? e.g. (let-alist '((foo . ((bar . "llama") (baz . "monkey")))) .foo.bar) will return "llama". I guess you wrote let-alist after the question was asked, but it's in the spirit of the question and very worth mentionning IMO! – YoungFrog Aug 25 '15 at 14:47
13

Here's an option which takes the exact syntax that you asked for but in a generalized way, and is quite simple to understand. The only difference is that the ALIST parameter needs to come first (you could adapt it to come last, if that's important to you).

(defun assoc-recursive (alist &rest keys)
  "Recursively find KEYs in ALIST."
  (while keys
    (setq alist (cdr (assoc (pop keys) alist))))
  alist)

Then you can call it with:

(assoc-recursive x 'foo 'bar)
  • 2
    This is more or less what I had cooked up as well. I'm a bit surprised that this isn't part of some established library like dash or something. It seems to come up all the time when dealing with e.g. json data. – abingham Nov 7 '14 at 5:49
2

Here's a more generic solution:

(defun assoc-multi-key (path nested-alist)
   "Find element in nested alist by path."
   (if (equal nested-alist nil)
       (error "cannot lookup in empty list"))
   (let ((key (car path))
         (remainder (cdr path)))
     (if (equal remainder nil)
         (assoc key nested-alist)
       (assoc-multi-key remainder (assoc key nested-alist)))))

It can take any "path" of keys. This will return (bar . "llama")

(assoc-multi-key '(foo bar)
    '((foo (bar . "llama") (baz . "monkey"))))

whereas this will return (baz . "monkey"):

(assoc-multi-key '(foo bar baz)
    '((foo (bar (bozo . "llama") (baz . "monkey")))))
  • 3
    Got my first downvote for this answer. Anyone willing to tell me why? – rekado Nov 7 '14 at 6:55
  • 1
    I disagree with the downvote since your code works (+1). My speculation is that @Malabarba's answer is clearly more general/elegant than the other answers on offer, and so the other answers received downvotes not because they don't work, but because they're not the best one. (That being said, I prefer the "upvote the best" option rather than "upvote the best and downvote the others" alternative.) – Dan Nov 7 '14 at 11:23
  • 1
    These two questions were downvoted because there is one person here who doesn't quite understand how downvotes work (and chooses to disregard the interface's request to leave a comment). It's unfortunate, but the best we all can do is upvote. – Malabarba Nov 7 '14 at 12:00
0

Here's a simple function that works with an alist nested inside another alist:

(defun assoc2 (outer inner alist)
  "`assoc', but for an assoc list inside an assoc list."
  (assoc inner (assoc outer alist)))

(setq alist2 '((puppies (tail . "waggly") (ears . "floppy"))
               (kitties (paws . "fuzzy")  (coat . "sleek"))))

(assoc2 'kitties 'coat alist2)       ;; => (coat . "sleek")
(cdr (assoc2 'kitties 'coat alist2)) ;; => "sleek"
  • 3
    Please people, when you down vote, leave a comment. – Malabarba Nov 7 '14 at 5:13
  • 1
    Whoever downvoted: I'm not offended, but am curious why. @Malabara: there's now a meta thread on norms on "downvote + comment"?; I'd be curious on your take. – Dan Nov 7 '14 at 11:16

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.