更过程式的let——vertical-let

26次阅读

共计 1586 个字符,预计需要花费 4 分钟才能阅读完成。

作为一名自诩的 non-trivial 的 Common Lisp 程序员,在编码的时候经常会遇到令人不愉快的地方,其中一个便是 LET。
一段典型的 LET 的示例代码如下
(let ((a 1))
a)
大多数时候,LET 不会只有一个绑定。并且,也不会只是绑定一个常量这么简单,而应当是下面这样的
(let ((a (foo x y))
(b (bar z)))
(function1 a b)
(function2 a b))
有时候我会想看看某一个绑定的值——最好是在它计算完毕后立即查看。如果要查看 foo 函数的返回值,可以这样写
(let ((a (foo x y))
(b (bar z)))
(print a)
(function1 a b)
(function2 a b))
如果调用 foo 和 bar 都顺利的话上面的代码也就够了。比较棘手的情况是,如果 a 的值不符合预期,会导致 b 的计算过程出状况(尽管在上面的代码中看似不会)。这种情况多出现在 LET* 的使用中,如下面所示
(let* ((a (foo x y))
(b (bar a)))
(function1 a b)
(function2 a b))
如果错误的 a 会导致 bar 的调用出错,那么在调用 function1 之前才调用 print 打印 a 已经为时过晚了——毕竟调用 bar 的时候就抛出 condition 往调用链的上游走了。一种方法是写成下面这样子
(let* ((a (let ((tmp (foo x y)))
(print tmp)
tmp))
(b (bar a)))
(function1 a b)
(function2 a b))
这也太丑了!要不然写成下面这样子?
(let ((a (foo x y)))
(print a)
(let ((b (bar a)))
(function1 a b)
(function2 a b)))
本来一个 LET 就可以做到的事情,这下子用了两个,还导致缩进更深了一级。如果有十个变量需要打印,就会增加十个 LET 和十层缩进。如果心血来潮想查看一个变量的值,还要大幅调整代码。
问题大概就出在 LET 和 LET* 的语法上。以 LET 为例,它由截然分开的 bindings 和 forms 组成,两者不能互相穿插。因此,如果想在 bindings 中求值一段表达式,只能将 bindings 切开,写成两个 LET 的形式。好像写一个新的宏可以解决这个问题?是的,vertical-let 就是。
vertical-let 是一个我自己写的宏,源代码在此。其用法如下
(vertical-let
:with a = 1
a)
它借鉴了 LOOP 中绑定变量的方式(即:with 和 =),绑定变量和用于求值的代码还可以交织在一起,如下
(vertical-let
:with a = 1
(print a)
:with b = 2
(+ a b))
vertical-let 最终会展开为 LET,比如上面的代码,会展开为如下的代码
(LET ((A 1))
(PRINT A)
(LET ((B 2))
(+ A B)))
vertical-let 的算法很简单。它遍历表达式列表,当遇到:with 时就把接下来的三个元素分别视为变量名、等号,以及待求值的表达式,将三者打包进一个列表中,再压栈;当遇到其它值时,就视为待求值的表达式(将会组成 LET 的 forms 部分),也放进列表中再压栈(具体方法参见源代码)。
将所有值都遍历并压栈后,接下来要遍历这个栈中的元素。先准备两个空的栈——一个存放 bindings,一个存放 forms。接着,对于每一个从栈中弹出的元素,分为如下两种情况:

如果表示 binding,则直接压入存放 bindings 的栈,否则;
如果是待求值的表达式,并且上一个出栈的元素是 binding,则说明已经有一段完整的 LET 的内容被集齐。因此,将目前在两个栈中的内容全部弹出,组合为一个 LET 表达式再压入存放 forms 的栈中。然后将方才弹出的表达式也压入 forms。重复上述过程直至所有元素都被处理,最后将还在两个栈中的内容也组合为一个 LET 表达式便结束了。

全文完

正文完
 0