To fold a flat list in Lisp you use reduce
:
* (reduce #\'+ \'(1 2 3 4 5))
15
But what if I have an arbitrarily complex tree, a
In addition to developing a tree-reduce
, a useful exercise is to try to repair your existing code so that it is more generally applicable.
That is, we take what you have:
(defun tree+ (a b)
(cond ((null b) 0)
((atom b) (+ a b))
(t (+ (tree+ a (car b))
(tree+ 0 (cdr b))))))
(reduce #'tree+ '(1 (2) (3 (4) 5)) :initial-value 0)
Note how we are just passing #'tree+
into reduce
, and tree+
is hard-coded for addition. The obvious fix is is to curry the +
function as a functional argument.
To achieve this, we can very simply transform the bulk your tree+
into a function that returns a function.
We don't use lambda
, because then we would need a Y-combinator or other silly hack to handle the recursion, which is much more easily achieved by using labels
to our function a local name:
(defun tree-reducer (binary-func &optional initial-val)
(labels ((tr-red (a b)
(cond ((null b) initial-val)
((atom b) (funcall binary-func a b))
(t (+ (tr-red a (car b))
(tr-red initial-val (cdr b)))))))
#'tr-red))
Now this is used like this:
(reduce (tree-reducer #'+ 0) '(1 (2) (3 (4) 5)) :initial-value 0) -> 15
Unfortunately, the initial value is specified twice, the reason for this being that the function returned by tree-reducer
takes on some of the responsibility of carrying out the reduce logic! Note that when we add a level of nesting to the tree and call:
(reduce (tree-reducer #'+ 0) '((1 (2) (3 (4) 5))) :initial-value 0) -> 15
who is doing the work of producing 15? Not the reduce
function! All it does is call the function once, with the arguments ((1 (2) ...)))
and 0
, which then does all the work.
Also, the initial-value argument of tree-reducer
will seriously misbehave if it is not the identity element for the given function (like zero to addition).
(reduce (tree-reducer #'+ 0) '(1 (2) (3 (4) 5)) :initial-value 1) -> 16 ;; OK
(reduce (tree-reducer #'+ 1) '(1 (2) (3 (4) 5)) :initial-value 0) -> 20 ;; Whoa!