草,我是不是应该在帖子开头就先介绍一下这几个宏比较好,免得看的一头雾水。
这里使用当前最新 Emacs 中的 foo-let
进行介绍,希望能让不熟悉的读者简单了解其使用方法与内部实现。这些宏包括 if-let/let*
, when-let/let*
, and-let*
和 Emacs 29 新添加的 while-let
。
if-let*
除 and-let*
外,其他的 foo-let
都在内部使用了 if-let*
。这是它的参数列表和 docstring
:
(defmacro if-let* (varlist then &rest else)
"Bind variables according to VARLIST and evaluate THEN or ELSE.
Evaluate each binding in turn, as in `let*', stopping if a
binding value is nil. If all are non-nil return the value of
THEN, otherwise the value of the last form in ELSE, or nil if
there are none.
Each element of VARLIST is a list (SYMBOL VALUEFORM) that binds
SYMBOL to the value of VALUEFORM. An element can additionally be
of the form (VALUEFORM), which is evaluated and checked for nil;
i.e. SYMBOL can be omitted if only the test result is of
interest. It can also be of the form SYMBOL, then the binding of
SYMBOL is checked for nil."
...)
可见, if-let*
是 if
和 let*
的结合体,首先对 VARLIST
进行 let*
绑定,然后根据绑定的值判断执行 THEN
还是 ELSE
表达式。 if-let*
对绑定求值的描述是“逐个求值且在遇到空值时停止,若绑定无空值则返回 THEN
的值,否则返回 ELSE
的值或空值”。在 VARLIST
中的绑定除了是 (SYMBOL VALUEFORM)
形式外,还可以省略掉绑定名字为 (VALUEFORM)
。以下例子可以简单说明 if-let*
的用法:
(if-let* ((x 1)
(y (+ x 1)))
(+ x y) x)
=> 3
(if-let* ((x 1)
((booleanp x)))
x (+ x 1))
=> 2
需要注意的是,“遇到空值时停止”意味着 if-let*
是 短路求值 的,它并不会对所有绑定求值后再判断绑定是否全为真,以下宏展开结果可以说明这一点:
(macroexpand '(if-let* ((x 1) (y 2)) (+ x y) x))
=> (let* ((x (and t 1))
(y (and x 2)))
(if y (+ x y) x))
最后, if-let*
表达式的值是 THEN
或 ELSE
表达式的值,若绑定中存在空值且无 ELSE
表达式,那么 if-let*
表达式的值为空值:
(if-let* ((nil)) 1) => nil
when-let*
和 and-let*
when-let*
就是不带 ELSE
的 if-let*
,它的实现如下所示:
(defmacro when-let* (varlist &rest body)
"Bind variables according to VARLIST and conditionally evaluate BODY.
Evaluate each binding in turn, stopping if a binding value is nil.
If all are non-nil, return the value of the last form in BODY.
The variable list VARLIST is the same as in `if-let*'.
See also `and-let*'."
(declare (indent 1) (debug if-let*))
(list 'if-let* varlist (macroexp-progn body)))
and-let*
的实现与 if-let*
非常相似,但是存在一些细微差异,它的 docstring
说明了这一点:
(defmacro and-let* (varlist &rest body)
"Bind variables according to VARLIST and conditionally evaluate BODY.
Like `when-let*', except if BODY is empty and all the bindings
are non-nil, then the result is the value of the last binding.
Some Lisp programmers follow the convention that `and' and `and-let*'
are for forms evaluated for return value, and `when' and `when-let*' are
for forms evaluated for side-effect with returned values ignored."
;; ^ Document this convention here because it explains why we have
;; both `when-let*' and `and-let*' (in addition to the additional
;; feature of `and-let*' when BODY is empty).
...)
可见若 and-let*
的 BODY
为空且绑定全非空时它将返回绑定的最后一个表达式,而不是返回空值。 docstring
也提到了 and-let*
更关注表达式的值,而 when-let*
更关注副作用。以下是一些简单的 and-let*
例子:
(and-let* ((x 1) (y 2) ((+ x y))))
=> 3
(and-let* ((x '(1 2 3))
((not (null x))))
(cdr x))
=> (2 3)
if-let
和 when-let
就像本文开头提到的那样, if-let
和 when-let
在最新的 Emacs 中被废弃了,说不好会在几个 Emacs 主版本后被移除。以下是它们的实现:
(defmacro if-let (spec then &rest else)
"Bind variables according to SPEC and evaluate THEN or ELSE.
This is like `if-let*' except, as a special case, interpret a SPEC of
the form \(SYMBOL SOMETHING) like \((SYMBOL SOMETHING)). This exists
for backward compatibility with an old syntax that accepted only one
binding."
(declare (indent 2)
(debug ([&or (symbolp form) ; must be first, Bug#48489
(&rest [&or symbolp (symbolp form) (form)])]
body))
(obsolete if-let* "31.1"))
(when (and (<= (length spec) 2)
(not (listp (car spec))))
;; Adjust the single binding case
(setq spec (list spec)))
(list 'if-let* spec then (macroexp-progn else)))
(defmacro when-let (spec &rest body)
"Bind variables according to SPEC and conditionally evaluate BODY.
Evaluate each binding in turn, stopping if a binding value is nil.
If all are non-nil, return the value of the last form in BODY.
The variable list SPEC is the same as in `if-let'."
(declare (indent 1) (debug if-let)
(obsolete "use `when-let*' or `and-let*' instead." "31.1"))
;; Previously we expanded to `if-let', and then required a
;; `with-suppressed-warnings' to avoid doubling up the obsoletion
;; warnings. But that triggers a bytecompiler bug; see bug#74530.
;; So for now we reimplement `if-let' here.
(when (and (<= (length spec) 2)
(not (listp (car spec))))
(setq spec (list spec)))
(list 'if-let* spec (macroexp-progn body)))
与各自的 let*
版本不同的是, if/when-let
支持当仅存在一个绑定时去掉 VARLIST
的外层括号,即允许如 (if-let (x 1) x 0)
的写法。在当前这个版本这大概是唯一的区别了,其他没什么好说的。
while-let
while-let
相比起前面几个宏就比较新了,在 29.1 才加入 Emacs,它的实现如下:
(defmacro while-let (spec &rest body)
"Bind variables according to SPEC and conditionally evaluate BODY.
Evaluate each binding in turn, stopping if a binding value is nil.
If all bindings are non-nil, eval BODY and repeat.
The variable list SPEC is the same as in `if-let*'."
(declare (indent 1) (debug if-let))
(let ((done (gensym "done")))
`(catch ',done
(while t
;; This is `if-let*', not `if-let', deliberately, despite the
;; name of this macro. See bug#60758.
(if-let* ,spec
(progn
,@body)
(throw ',done nil))))))
这里有两点需要注意:(1) while-let
使用了 let*
而不是它名字中的 let
bug#60758: 29.0.60; while-let uses if-let* convention in contradiction t ;(2) let*
绑定发生在 while
内而不是 while
外,这与 if/when/and-let*
行为并不一致,第一次使用时可能会感觉非常反直觉:Is this a bug in while-let or do I missunderstand it?。
(while-let ((run t))
(setq run nil))
=> infinite loop...
根据 while-let
的实现来看,每次循环都会重新执行 if-let*
绑定,直到绑定中出现空值。这意味着我们无法在绑定完成后通过 直接修改绑定值 来终止循环。如果想要仅通过绑定为空来终止循环,绑定对应的表达式需要最终变化为空值,而不能全是非空常值:
(let* ((ls1 (list 1 2 3))
(ls2 (copy-sequence ls1)))
(list (let (curr-v (res 0))
(while (setq curr-v (pop ls1))
(cl-incf res curr-v))
res)
(let ((res 0))
(while-let ((curr-v (pop ls2)))
(cl-incf res curr-v))
res)))
=> (6 6)
当前 Emacs 中使用 while-let
的代码主要集中在 ERC 中,读者可以找找更复杂的使用例子: