首页 > 解决方案 > 让的用处?

问题描述

3 种不同的 let 形式(如 Scheme 的 let、let* 和 letrec)之间的区别在实践中有用吗?

我目前正在开发一种目前支持所有 3 种形式的 lisp 风格的语言,但我发现:

  1. 常规的“let”是效率最低的形式,实际上必须转换为立即调用的 lambda 形式,并且生成的指令几乎相同。此外,我发现自己并不经常需要这种形式。

  2. let*(顺序绑定)似乎是最实用和最常用的。这种形式可以翻译成一系列嵌套的“let”,每个环境都存储一个变量。但这又是非常低效的,浪费空间和查找时间。

  3. letrec(递归绑定)可以有效地实现,因为没有初始化表达式引用未绑定的变量。通常情况是所有初始化程序都是 lambda 表达式,并且上述情况是正确的。

问题是:既然 letrec 可以有效地实现并且还包含了 let* 的行为,那么常规的 let 不经常使用并且可以转换为 lambda 形式而不会造成很大的效率损失,为什么不让默认的“let”具有这种行为当前的“letrec”并摆脱原来的“let”?

标签: lisp

解决方案


这种 [ let*] 形式可以翻译成一系列嵌套的“let”,每个环境都存储一个变量。但这又是非常低效的,浪费空间和查找时间。

虽然你在这里说的没有错,但实际上没有必要进行这样的转换。简单的编译策略可以通过简单let的修改来处理语义let*(可能只通过传递给公共代码的标志来支持两者)。

let*只是更改在编译时确定的范围规则;在编译给定的变量init形式时,主要是使用哪个编译时环境对象。

编译器可以将单个环境对象用于 a 的顺序绑定let*,并在编译变量init形式时对其进行破坏性更新,以便每个连续的init形式看到该环境的越来越多的扩展版本,其中包含越来越多的变量。最后,可以使用包含所有变量的完整环境,用于生成框架和诸如此类的代码生成。

需要注意的一个问题是,平面环境表示let*意味着在变量绑定阶段捕获的词法闭包可以捕获未来在词法上不可见的变量:

(let* ((past 42)
       (present (lambda () (do-something-with past)))
       (future (construct-huge-cumbersome-object)))
  ...))

如果这里有一个单独的运行时环境对象,其中包含变量和的编译版本past,则意味着必须捕获该环境。这意味着虽然表面上“看到”的只是变量,但因为不在范围内,它实际上已经捕获了.presentfuturelambdalambdapastfuturefuture

因此,垃圾收集将认为巨大的繁琐对象是可访问的,只要lambda保持可访问。

有一些方法可以解决这个问题,比如伴随着从 发出的环境参考lambda和某种帧索引,它说:“我只参考了索引 13 之前的环境向量的一部分”。然后当垃圾收集器遍历这个被隔离的引用时,它只会标记环境向量的指示部分:单元格 0 到 13。

无论如何,关于是否同时实现letlet*。我怀疑如果今天 Lisp 是从头开始设计的“绿色领域”,许多设计人员都希望能够调用顺序绑定版本let。并行结构将是一个特殊名称下可用的结构let*。实际需要let并行的情况较少。例如,let允许我们重新绑定一对变量符号,以使它们的内容看起来是交换的;但这在应用程序编程中很少出现。在某些编程语言文化中,变量阴影完全不被接受。例如, GNU C 有一个-Wshadow警告。

请注意在具有let和的 ANSI Common Lisp 中,let*函数的可选参数如何按顺序表现,例如let*,这是唯一支持的绑定策略!也就是说:

(lambda (required &optional opt1 (opt2 opt1)) ...)

这里的值opt2默认为opt1调用时的值。的初始化表达式opt2具有opt1范围内的参数。

此外,在同一个 Lisp 方言中,正则setf是顺序的;如果要并行分配,则必须使用psetf,这是两者中较长的名称。

letCommon Lisp 已经展示了比倾向于顺序操作更近的设计决策的证据,并将并行指定为非凡的变体。


推荐阅读