从提议电子邮件复制到Clojure/dev Google群体
https://groups.google.com/d/msg/clojure-dev/zlMGmv60MVA/beyIRTrhAgAJ
你好,
目前loop/recur仅限于“单层”循环:循环形式可以出现在其他循环形式内,但没有任何“回溯到外部循环”的设施。
几年前,我发表了一项关于将嵌套循环添加到Clojure的提案,并提供了一个ClojureScript的概念验证补丁,其语法和语义足以使嵌套循环感觉自然,同时仍然是核心loop/recur模型的自然扩展,具有相同的显式尾递归优势。
(loop foo [...] ; 命名循环
...
(loop [...] 陌生人循环
...
(loop [...] 最内层循环
...
(recur-to foo ...)))) ; 回溯到命名循环;
NB. 这必须在尾部位置
与三个循环相关
https://groups.google.com/d/msg/clojure-dev/imtbY1uqpIc/8DWLw8Ymf4IJ
https://dev.clojure.org/display/design/Named+loops+with+recur-to
https://github.com/michalmarczyk/clojurescript/tree/recur-to
https://github.com/michalmarczyk/clojurescript/commit/feba0a078138da08d584a67e671415fc403fa093
我现在已在Clojure中实现了一个完整的补丁,使得提议的功能得以实现(第一个链接是基于当前master的分支,即1.9.0-alpha17之后的“为下一次开发迭代做准备”提交;第二个是此分支的当前尖端,供将来参考)
https://github.com/michalmarczyk/clojure/tree/recur-to
https://github.com/michalmarczyk/clojure/commit/212ea06d21d3b336ac35480c99170e81defaf956
我还在JIRA中创建了一个票据,以便将上述内容以补丁文件的形式发布
https://dev.clojure.org/jira/browse/CLJ-2235
此电子邮件的其余部分更详细地介绍了提议,以一种相对严格的形式陈述了其关键属性,简要总结了实现方法,并讨论了补丁中做出的某些设计选择。
概述
========
例如,人们可以编写:
(let [m (two-dimensional-matrix)]
(loop iloop [i 0] 命名循环
(if (< i i-lim)
(loop [j 0] 嵌套匿名循环
(if (< j j-lim)
(do
(work)
(recur (inc j))) ; 递归到最内层循环
(recur-to iloop (inc i)))) ; 递归到 iloop
(done))))
并且,在递归形态相对于所有其包含的循环形态直到包含目标(可能是命名循环或fn形态)都是尾形态,并且传递给每个递归形态的目标参数的数量与目标循环的循环局部变量数量匹配(加上一个作为先导循环名称参数),则这应该能够编译并且具有与Java中嵌套循环类似的性能。
提出的语法是模仿了Scheme的命名let,虽然在语义上
这二者有所不同——这一提议严格限于以自然的方式扩展循环/递归模型到嵌套循环。当然,命名fn形态也应该作为有效的recur-to目标。
命名循环和recur-to的关键特性
==========================================
在上述补丁放置到位后,以下规则在编译时生效
1. 每个recur-to形态必须相对于所有其包含的循环形态(无论是否有名)处于尾形态,直到包括其目标(可能是一个命名循环或fn形态)。
2. 如果recur-to的目标不出现在其位于尾形态的包含循环或fn形态的名字中,则这是一个错误。
3. 不能通过try来递归。
4. 传递给recur-to的参数数量(超出初始目标/标签参数外)必须与目标循环或fn形态的形式参数数量相匹配。
5. 允许循环名称的遮蔽;recur-to只能针对与其尾形态存在的给定名称的最内层循环。被遮蔽的命名的循环引入的循环局部变量在遮蔽循环内仍然可见(除非它们被相同名称的局部变量遮蔽)。
注意。循环名称不是局部变量。特别是,它们既不被局部变量遮蔽,也不是被同名局部变量遮蔽。这一点值得详细讨论;请参阅本电子邮件末尾的设计选择部分。
最内层循环或fn形态始终可以使用 recur 进行定位,无论其是否有名。此外(recur-to nil ...) 与 (recur ...) 等价(即使在最内层循环或fn形态实际上是命名的情况下),并且 (loop nil [...] ...) 与 (loop [...]) 等价。
实现方法的概述
======================================
该补丁修改了编译器中循环标签的处理,并实现了对循环宏的少数必要的调整。
它还引入了循环*特殊形式的可选名称参数。(这主要是为了避免破坏直接发射loop*的非核心宏。)
最后,将 recur 特殊形式重命名为 recur*;recur 和 recur-to 成为定义在 clojure.core 中的宏。请参阅下文设计选择部分以了解其他方法。
设计选择
==============
1. 在开发过程中,纯粹是为了方便,那时我有一个单独的 loop-as 宏,它接受一个名称参数。我认为将命名功能直接添加到 loop 中是合理的,特别是考虑到 fn 已经接受一个可选的名称。不过,loop-as仍然是一个有效的设计选择。
2. 如果避免重命名现有的 recur 特殊形式为 recur* 并重新实现 recur 作为宏是一个愿望,可以添加一个新的 recur-to 特殊形式。(或者,可以在保留 recur 的同时添加 recur-to 作为将 recur* 特殊形式委托给新 recur* 特殊形式的宏。)
3. 如果未来希望保留将循环名称视为局部变量的选项,那么现在将其视为隐藏并在局部变量上被隐藏可能是更可取的,因为否则在后来将它们提升为局部变量的状态将会造成破坏性变更。为了举例说明这种未来的更改可能有用,如果尾调用消除支持在未来 JDK 规范中到来,人们可能会考虑是否采用类似于 Scheme 的方法,其中循环名称被视为绑定到只有一个 arglist(对应于命名循环的循环局部变量)的函数的局部变量;如果局部变量从未被引用,这种闭包分配可能会被优化掉。
需要注意的是,如果尾调用消除支持真的实现了,它将使一系列替代设计成为可能。例如,可以引入 Scheme 风格的命名 let 作为 let 宏的功能之一。因此,在我看来,将 loop/recur/recur-to 限制为仅用于 label+goto 风格的循环是有道理的,即使在假设的未来的 VM 尾调用消除支持中,也没有理由给予循环名称类似局部的待遇;因此,补丁当前采用不进行隐藏交云的方法。
祝好,
Michał