12

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.

6
  • 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, 2014 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, 2014 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, 2014 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, 2014 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, 2015 at 14:47

3 Answers 3

17

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)
1
  • 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, 2014 at 5:49
3

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
  • 3
    Got my first downvote for this answer. Anyone willing to tell me why?
    – user2005
    Nov 7, 2014 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, 2014 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, 2014 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"
2
  • 3
    Please people, when you down vote, leave a comment.
    – Malabarba
    Nov 7, 2014 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, 2014 at 11:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.