不考虑car部分计算开销时,用Stream实现Lazy Tree是否可行?
背景:SICP与Hughes论文的关联
注意,这些惰性列表比第3章的stream更‘惰性’:列表的car和cdr均被延迟计算。^[41]
[41] 这使得我们可以创建更通用列表结构的延迟版本,而非仅局限于序列。Hughes 1990年的论文探讨了‘lazy trees’的若干应用。
Hughes的论文中,第3章聚焦树的结构、第5章讲解树的应用,均涉及lazy tree(可无限扩展的游戏树)。
Stream和完全惰性列表的核心差异在于:Stream会立即求值car部分,而完全惰性列表的car和cdr都延迟计算。但针对论文定义的树结构treeof ∗ ::= Node ∗ (listof (treeof ∗))(类似(cons node-value subtrees)),由于节点值(node-value)通常不会触发无限循环,求值car是完全可行的。因此即使Stream会因立即求值car产生少量额外开销,依然可以用来实现lazy tree。
论文中Lazy Tree与惰性求值的核心逻辑
论文中关于lazy tree的关键代码逻辑如下:
其中“.”(函数复合,标准运算符)定义为:(f . g) h = f (g h)
...
maximize = max . maximize'
maximize'(Node n Nil) = Cons n Nil
maximize'(Node n l) = map minimize l
...
evaluate = maximize . maptree static . prune 5 . gametree
...
更重要的是,惰性求值允许我们以这种方式模块化evaluate。由于gametree可能生成无限结果,若无惰性求值,该程序将永远无法终止。
对于Stream而言,maptree static . prune 5 . gametree的结果是一个Stream。其中minimize的定义与maximize对称:maximize本质是带短路优化的深度优先搜索——当某子树的候选值不可能成为最终最大值时,会停止遍历该分支(比如第二个子树的最小值小于第一个子树的最小值,就无需继续遍历第二个子树)。不过这种优化不适用于无限宽度且严格递增的树结构。
由于prune 5会将无限树截断为深度有限的结构,maptree static . prune 5 . gametree实际是一个有限Stream,常规的maximize迭代逻辑可以正常运行,且Stream本身的惰性特性足以支撑整个流程:
正如prune仅查看无限树的部分内容,确保了程序能够终止
正式技术问题
在不考虑Stream立即求值car带来的潜在计算开销的前提下,是否可以使用Stream来实现lazy tree?
基于Stream的代码实现
要实现上述evaluate,需要max, map, Node, maptree, static, prune及树的基础辅助过程:
max和static可直接使用库函数map采用SICP中的stream-mapNode用cons-stream模拟- 根据论文中
foldtree的定义,maptree可定义为maptree f = foldtree (Node . f ) Cons Nil(此处Node和Cons均可替换为cons-stream)
由于树是通过Node、Cons和Nil构建的,foldtree必须接受三个参数
以下是prune的Stream版本实现(对应论文定义:prune 0 (Node a x) = Node a Nil、prune (n + 1) (Node a x) = Node a (map (prune n) x)):
(define (prune max-depth tree) (if (= 0 max-depth) (cons-stream (stream-car tree) the-empty-stream) (cons-stream (stream-car tree) (stream-map (lambda (subtree) (prune (- max-depth 1) subtree)) (stream-cdr tree)))) ) (define infinite-width-leaves (cons-stream (cons-stream 1 the-empty-stream) infinite-width-leaves)) (define demo-tree-with-infinite-depth-and-width (cons-stream 0 (cons-stream demo-tree-with-infinite-depth-and-width infinite-width-leaves))) (define test-stream-2 (prune 10 demo-tree-with-infinite-depth-and-width)) test-stream-2 ;Value: {0 ...} (stream->list (prune 0 demo-tree-with-infinite-depth-and-width)) ;Value: (0)
论文相关补充笔记
Lazy特性相关
只有函数式语言(且并非所有函数式语言)会对每个函数调用统一使用惰性求值
基于Stream构建的tree库同样可以实现lazy tree。由于maximize处理完每个部分后即可将其丢弃(由垃圾回收器回收),整棵树永远不会驻留在内存中。同一时间仅存储树的一小部分,因此惰性程序效率很高。
...
得益于惰性求值,若maximize并未查看所有数值列表,部分数值将不会被计算
这是惰性求值结合垃圾回收的效果,Stream同样可以实现该特性——未被访问的Stream部分不会驻留内存,未被用到的计算也不会触发。这种效率依赖于maximize(复合链中的最后一个函数)与gametree(第一个函数)之间的交互,若无惰性求值则无法实现
复合链会无限生成游戏树的每一层,但Stream仅在需要时才求值节点值,不会提前计算子树,因此可以实现这种按需交互。得益于惰性求值,maximize'仅查看树的部分内容,使得整个程序运行效率更高
maximize'通过静态值与minleq的关系丢弃部分子树,这种短路逻辑可以通过Stream实现:子树以Stream形式存在,未被遍历的部分不会被求值。
无限结构相关
repeat函数是输出“无限”结果的示例
repeat函数用于构造无限列表
integrate函数计算一个无限列表,包含积分的一系列逐步优化的近似值
这与无限Stream的特性完全一致。它不适用于无限树,因为maximize会持续递归直至找到无子树的节点
若游戏可能永远进行且双方均无法获胜,则其游戏树是无限的
这对应无限深度的树结构,Stream可以通过按需求值来处理这种无限结构。静态求值树中的所有位置(可能数量无限)
这对应无限宽度/深度的情况,Stream的惰性特性可以支持这种无限结构的按需计算。首先,它不适用于无限树,因为maximize会持续递归
惰性求值是关于参数求值而非过程调用的,Stream的按需求值逻辑可以避免无限递归导致的程序终止问题。
内容的提问来源于stack exchange,提问作者An5Drama

