要在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
是一般函数,有时您必须保留原始序列的结构和顺序。例如,map
和filter
可以使用reduce
实现。如果我想基于my-map
编写reduce
的实现,那么该如何:
(my-map '1+ '(1 (2 (3) 4) 5)) ; outputs '(2 (3 (4) 5) 6)
如何在树结构上使用reduce
?在树上应用二进制函数的最通用方法是什么?
答案 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)))
或者,您可以将Nil
和init
想象为零参数函数,在这种情况下,列表将变为
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没有map
或reduce
的树版本。
事实上,我能记得的唯一树功能是tree-equal
和subst
。
但是,做这样的事情应该不会太难:
(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) ...)))
功能!所有这一切都是使用参数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!
的初始值参数如果不是给定函数的标识元素(如零加法),将严重行为错误。
{{1}}