在Lisp中使用reduce over a tree

时间:2014-07-13 07:20:37

标签: tree lisp common-lisp fold

要在Lisp中折叠平面列表,请使用reduce

* (reduce #'+ '(1 2 3 4 5))
15

但是如果我有一个任意复杂的树,我想在每个元素上应用一个函数呢?那么折叠'(1 (2) (3 (4) 5))仍会给15?我尝试使用reduce,但我必须提供自定义函数,这有点违背了目的:

(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) ; returns 15

当然我可以先将列表展平,但reduce是一般函数,有时您必须保留原始序列的结构和顺序。例如,mapfilter可以使用reduce实现。如果我想基于my-map编写reduce的实现,那么该如何:

(my-map '1+ '(1 (2 (3) 4) 5)) ; outputs '(2 (3 (4) 5) 6)

如何在树结构上使用reduce?在树上应用二进制函数的最通用方法是什么?

3 个答案:

答案 0 :(得分:4)

我在Counting elements of a list and sublists中提供了 treeduce 功能的实现,虽然它适用于Scheme,但这里适用相同的原则。维基百科在Fold (higher-order function)中说:

  

在函数式编程中,fold - 也称为reduce,   累积,聚合,压缩或注入 - 指的是一个家庭   分析递归数据结构的高阶函数   通过使用给定的组合操作重新组合结果   递归处理其组成部分,建立一个回报   值。通常,折叠呈现组合功能,顶部   数据结构的节点,可能还有一些默认值   在一定条件下。然后折叠继续组合元素   数据结构的层次结构,使用系统中的功能   方式。

列表数据结构可以描述为代数数据类型:

List ::= Cons(Object, List)
       | Nil

当我们使用函数调用reduce列表时,我们基本上将Cons的每次使用转换为函数的应用程序,并且每次使用Nil都具有一些常量值。也就是说,我们采取列表

Cons(x,Cons(y,Cons(z,Nil)))

并将其转换为

Fn(x,Fn(y,Fn(z,init)))

或者,您可以将Nilinit想象为零参数函数,在这种情况下,列表将变为

Fn(x,Fn(y,Fn(z,init())))

对于树木,我们可以做同样的事情,但它有点复杂。对于树,代数数据类型为:

Tree ::= Node(Tree,Tree)
       | Leaf(Object)

要对树进行缩减,我们需要两个函数:一个用于替换Node,另一个用于替换Leaf。但定义非常简单:

TreeReduce(nodeFn,leafFn,tree) =
  case tree of 
    Node(left,right) => nodeFn(TreeReduce(nodeFn,leafFn,left),TreeReduce(nodeFn,leafFn,right)
    Leaf(object) => leafFn(object)

在Common Lisp中,简单地说:

(defun tree-reduce (node-fn leaf-fn tree)
  (if (consp tree)
      (funcall node-fn 
               (tree-reduce node-fn leaf-fn (car tree))
               (tree-reduce node-fn leaf-fn (cdr tree)))
      (funcall leaf-fn 
               tree)))
 
(tree-reduce 'cons
             (lambda (x) 
               (if (numberp x) (1+ x) x))
             '(1 (2 3) (4 5 6)))
;=> (2 (3 4) (5 6 7))

我们可以使用 tree-reduce 来计算您询问的总和:

(tree-reduce '+
             (lambda (x)
               (if (null x) 0 x))
             '(1 (2) (3 (4) 5)))
;=> 15

我们需要所有这些 null 警卫的原因是,当我们将基于cons的结构视为树时, nil 不是'真的有什么特别的。也就是说,我们可以考虑树(1(2.3)4.5)以及(1(2 3)4(5))(与(1(2 3 .nil)4(5)相同.nil).nil),当然)。

答案 1 :(得分:2)

Common Lisp没有mapreduce的树版本。 事实上,我能记得的唯一树功能是tree-equalsubst

但是,做这样的事情应该不会太难:

(defun reduce-tree (function tree &key (key #'identity))
  (if (atom tree)
      (funcall key tree)
      (funcall function
               (reduce-tree function (car tree) :key key)
               (reduce-tree function (cdr tree) :key key))))

试一试:

> (reduce-tree #'+ '(1 . ((2 . 3) . ((4 . 5) . 6))))
==> 21
> (reduce-tree #'+ '(1 (2) (3 (4) 5)) :key (lambda (x) (or x 0)))
==> 15

答案 2 :(得分:0)

除了开发tree-reduce之外,一个有用的练习是尝试修复现有代码,以便更普遍适用。

也就是说,我们拿走你拥有的东西:

(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)

请注意我们只是将#'tree+传递给reduce,而tree+是硬编码的,以便添加。显而易见的解决方法是将+函数作为函数参数进行讨论。

为了实现这一点,我们可以非常简单地将tree+的批量转换为返回函数的函数。

我们不使用lambda,因为我们需要一个Y-combinator或其他愚蠢的黑客来处理递归,这可以通过labels使用(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)) 来轻松实现本地名称:

(reduce (tree-reducer #'+ 0) '(1 (2) (3 (4) 5)) :initial-value 0)  -> 15

现在这样使用:

tree-reducer

不幸的是,初始值被指定了两次,原因是(reduce (tree-reducer #'+ 0) '((1 (2) (3 (4) 5))) :initial-value 0) -> 15 返回的函数承担了执行reduce逻辑的一些责任!请注意,当我们向树添加嵌套级别并调用时:

reduce
谁正在做15生产的工作?不是((1 (2) ...)))功能!所有这一切都是使用参数0tree-reducer调用函数一次,然后完成所有工作。

此外,(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! 的初始值参数如果不是给定函数的标识元素(如零加法),将严重行为错误。

{{1}}