《实战Common Lisp》系列主要讲述在使用Common Lisp时能派上用场的小函数,希望能为Common Lisp的复兴做一些微小的贡献。MAKE COMMON LISP GREAT AGAIN。
序言
因为觉得Common Lisp原生的let
操作符在许多时候不够好用,我编写了vertical-let
。(详情可以参见这篇文章)比起原生的let
,vertical-let
的优势在于:
- 有效减少代码的缩进——尤其是嵌套使用
let
的时候; - 方便增减binding,对其余代码的布局没有影响。
除了let
,destructuring-bind
也是一个常用的声明binding的语法。但如果用在vertical-let
中的话,会打乱原有的代码布局。比如原本的代码为
1 | (vertical-let |
如果加入destructuring-bind
,就会导致从它之后的代码都增加了一级缩进
1 | (vertical-let |
我更希望能写成下面这样
1 | (vertical-let |
然而vertical-let
目前的实现方式很难支持这种新语法。
在vertical-let
内部,将参数分成了“binding”和“form”两种类型,压入到同一个栈中,再逐一弹出处理。如果要支持展开成destructuring-bind
,那么:
- 如果弹出的是“binding”,就需要决定是将其与旧的合并(都是
let
的binding的情况),还是先处理已有的变量bindings
和forms
中的内容(这里又涉及到是组成let
还是组成destructuring-bind
); - 如果弹出的是“form”,也要考虑与上述场景类似的情况。
可想而知,这会让vertical-let
的代码膨胀得厉害,并且显得很混乱。因此,必须先优化一番vertical-let
。
重构vertical-let
新的思路是:
- 从尾部开始遍历
vertical-let
的参数列表; - 如果遍历到的元素不是符号
:with
,就认为是一个可以求值的表达式,将其压栈。显然,这个栈的元素的顺序,与vertical-let
的参数列表的顺序是一致的,可以直接用于合成let
表达式; - 如果遍历到的元素是符号
:with
,就从栈中弹出三个元素(它们依次是变量名、等号、待求值的表达式); - 将变量名、待求值的表达式,以及栈内所有元素组成只有一个binding的的
let
表达式,重新压栈。
当参数列表遍历完后,再看看这个栈:
- 如果只有一个元素,就是
vertical-let
的展开结果; - 否则,将它们作为
progn
的参数,返回一个progn
表达式。
支持destructuring-bind
在上面的算法中,遇到符号:with
后只需要构造出let
表达式即可。为了支持展开成destructuring-bind
,需要根据栈顶元素类型来做不同处理:
- 如果是
cons
,就展开为destructuring-bind
——毕竟destructuring-bind
是无法嵌套的; - 如果是
symbol
,就展开为let
(如果栈只有一个元素并且是let
表达式,那么可以将新的binding合并进去,减少展开后代码的缩进)。
现在,可以完整地实现vertical-let
了
1 | (defun vertical-let/aux (forms) |
后记
除了let
和destructuring-bind
,Common Lisp还提供了名为multiple-value-bind
的宏,用于捕捉从一个函数返回的多个值。如果又要修改vertical-let
的话,多半就是为了支持它了吧。