With dash
list and tree manipulation library you can convert an alist into a flat list by transforming a dotted pair (a . b)
into a 2-element list (a b)
, then flattening the list, which is possible with -mapcat
(the 2 expressions below are equivalent):
(-mapcat (lambda (x) (list (car x) (cdr x))) my-alist)
(-flatten (-map (lambda (x) (list (car x) (cdr x))) my-alist))
(apply '-concat (-map (lambda (x) (list (car x) (cdr x))) my-alist))
In Haskell similar expressions (successively apply various functions to transform a list) would be more efficient and natural due to lazy evaluation.
So I want to use this answer as an opportunity to explore programming concepts a bit. What is it, when you traverse a list once and build some result based on its content (no matter what result)? It's a fold! The most primitive form of fold is one that takes a binary function and applies it to elements 1 and 2, then to result and 3, then to result and 4, and so on. So fold accepting +
and list [1,2,3,4]
becomes ((1+2)+3)+4
. dash
has such kind of fold called -reduce
: (-reduce '+ '(1 2 3 4)) ; => 10
But this kind of fold is inflexible: a fold on a list of integers can only return an integer, not some arbitrary value. We need a more general fold, with better control. Such general fold uses an additional argument, called accumulator, which is used inside the binary function. On each iterator you can do anything with the list's element and the accumulator. The result of the function application becomes accumulator for the next iteration. In dash
such fold is called -reduce-from
. We take empty list as an accumulator, take each dotted pair in original alist one by one, then transform it into 2 new elements that we append to the accumulator inside our binary function. What could be easier?
(-reduce-from (lambda (acc x) (append acc (list (car x) (cdr x)))) '() my-alist)
But appending lists in such way is inefficient and idiomatic, because lists are implemented as singly linked lists in Lisp, so adding an element to the end or concatenating lists is O(n), the whole function works in O(n²). Lispers usually cons to the beginning of the list, but -reduce-from
traverses the list left-to-right, that's why the result is going to be reversed. If only we could traverse the list right-to-left, so that we could the element, tranform it, and cons to the accumulator. Well, there's a function -reduce-r-from
:
(-reduce-r-from (lambda (x acc) (cons (car x) (cons (cdr x) acc))) '() my-alist)
-reduce-r-from
is the most efficient version, as it traverses the alist only once. Every time you need to build some list using a fold, chances are you need -reduce-r-from
. Finally, what's great about the dash
library is that it provides anaphoric macro versions for functions that accept functions as arguments to get rid of lambda
syntax:
(--mapcat (list (car it) (cdr it)) my-alist)
(-flatten (--map (list (car it) (cdr it)) my-alist))
(apply '-concat (--map (list (car it) (cdr it)) my-alist))
(--reduce-r-from (cons (car it) (cons (cdr it) acc))) '() my-alist)