【问题标题】:Using reduce over a tree in Lisp在 Lisp 中对树使用 reduce
【发布时间】:2014-09-03 10:02:20
【问题描述】:

要在 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 实现。如果我想基于reduce编写my-map的实现,那么:

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

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

【问题讨论】:

标签: tree lisp common-lisp fold


【解决方案1】:

除了开发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 或其他愚蠢的 hack 来处理递归,这更容易通过在我们的函数中使用 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逻辑的部分责任!请注意,当我们向树添加一层嵌套并调用时:

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

谁在做 15 的工作?不是reduce 函数!它所做的只是调用函数一次,使用参数((1 (2) ...)))0,然后它会完成所有工作。

此外,如果 tree-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!

【讨论】:

    【解决方案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
    

    【讨论】:

    • 复制树也映射在一棵树上。我有,可能只是为了我自己的消遣,使用 nsubst 来映射一棵树。 (defun sum-tree (tree) (let ((sum 0)) (nsubst nil t tree :test #'(lambda (zip x) (declare (ignore zip))) (when (numberp x) (incf sum x))无))总和))毫无疑问,我会在来世为此受到惩罚。
    • 请注意,此定义不会让您执行(reduce-tree '+ tree),因为nil 是树的元素(与列表中的终止nil 不同)。它还对树中的元素和树中的节点使用相同的函数。 (很容易区分哪种情况,因为函数要么获取一个参数,要么获取两个参数,但折叠函数通常为每种类型获取一个参数,并且 key 已经用于处理元素。这样做可能就足够了(if (atom tree) (funcall key tree) …)
    • @JoshuaTaylor:我认为它总结了树叶就好了。
    • @sds 现在更清楚了,您已经添加了一些示例,这些示例突出了其中包含nil 的树与不包含nil 的树之间的区别。 OP 的原始树是 '(1 (2) (3 (4) 5)),而 (reduce-tree '+ '(1 (2) (3 (4) 5))) 不会在那里工作,因为树中有 nils。密钥为(lambda (x) (or x 0)) 的第二个示例显示了如何解决该问题。对于示例+1,尽管我仍然认为为“构造函数”设置单独的函数会更优雅一些;即一个替换 cons 的函数,以及一个用原子调用的函数(实际上是键)。我认为……
    • ...(funcall function (funcall key tree)) 多了一个它真正需要的函数调用。 key 函数不会在其他任何地方使用,因此它可以处理“用一个参数调用”,而不是让函数可以用一个和两个参数调用。
    【解决方案3】:

    我在Counting elements of a list and sublists 中提供了一个treeduce 函数的实现,虽然它是针对Scheme 的,但同样的原则也适用于这里。维基百科在Fold (higher-order function) 中说:

    在函数式编程中,折叠——也称为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),当然)。

    【讨论】:

      猜你喜欢
      • 2016-03-14
      • 2015-05-06
      • 2017-06-08
      • 2010-10-27
      • 2013-01-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-03-14
      相关资源
      最近更新 更多