-
Notifications
You must be signed in to change notification settings - Fork 11
Differences
(I was already planning to write this after a break but since I’ve now been asked to do so, I am rushing it a bit. I still have to proof-read this.)
This page lists differences between the built-in implementations of
macros such as if-let and and-let*, and the implementations of the
same macros in Cond-Let. I just describe the differences, not why
one might prefer one implementation over the other. Forms such as
cond-let and and$, which do not exist in Emacs, are not discussed
here.
Legend:
-
X-letmeans ”if-let,when-let,and-letand/orwhile-let”. -
X-let*means ”if-let*,when-let*,and-let*and/orwhile-let*”. -
if-...means ”if-letandif-let*”. - In code block
if-let, for example, always stands for the built-in implementation. For Cond-Let’s implementation the full name is used (e.g.,cond-let--if-let).
As implied by their names, Cond-Let’s X-let create bindings the same
way let does, while X-let* bind like let* does.
All built-in X-let and X-let* create bindings like let* does.
As a reminder, this is how let and let* differ:
-
let: All theVALUEFORMsare evalled before any symbols are bound. -
let*: EachVALUEFORMcan refer to the symbols already bound byVARLIST.
;; `let' vs `let'*
(let ((a 1))
(let ((a (1+ a))
(b (1+ a)))
(list a b)))
=> (2 2) [variant A]
(let ((a 1))
(let* ((a (1+ a))
(b (1+ a)))
(list a b)))
=> (2 3) [variant B]
;; Built-in `when-let' == `when-let*'
(let ((a 1))
(when-let ((a (1+ a))
(b (1+ a)))
(list a b)))
=> (2 3) [variant B] [sic]
(let ((a 1))
(when-let* ((a (1+ a))
(b (1+ a)))
(list a b)))
=> (2 3) [variant B]
;; Cond-Let's `when-let' vs `when-let*'
(let ((a 1))
(cond-let--when-let ((a (1+ a))
(b (1+ a)))
(list a b)))
=> (2 2) [variant A]
(let ((a 1))
(cond-let--when-let* ((a (1+ a))
(b (1+ a)))
(list a b)))
=> (2 3) [variant B]The bindings established by Cont-Let’s if-let and if-let* only extend
to THEN.
The bindings established by built-in if-let and if-let* extend to THEN
and ELSE.
(let ((x 1)
(y 1)
(z 1))
(if-let ((x 2)
(y nil)
(z 2))
(list 'then x y z)
(list 'else x y z)))
=> (else 2 nil nil)
(let ((x 1)
(y 1)
(z 1))
(cond-let--if-let ((x 2)
(y nil)
(z 2))
(list 'then x y z)
(list 'else x y z)))
=> (else 1 1 1)When the built-in if-let and when-let bind a single symbol, one pair
of parentheses can be omitted:
(if-let (a 1) ; same as (if-let ((a 1))
a
2)
(when-let (a 1) ; same as (when-let ((a 1))
a)The Emacs maintainers consider this a mistake (as do I). As far as
I understand, the ...-let* variants were in no small part created to
correct that mistake. The built-in X-let* and while-let do not allow
omitting a pair of parentheses.
Cond-Let’s implementations does not allow omitting a pair of
parentheses for any X-let or X-let*.
For both the build-in and Cond-Let’s implementations (of both
...-let and ...-let*) the canonical form of an element of VARLIST
is (SYMBOL VARFORM).
For the built-in ...-let* an entry of VARLIST may also be just SYMBOL.
However, for these forms treat such an element differently from how
let* does.
For let* (and let) SYMBOL as a direct element of VARLIST is equivalent
to (SYMBOL nil), while for the built-in ...-let* it is equivalent to
(SYMBOL SYMBOL).
(let* ((a nil))
a)
=> nil
(let* (a)
a)
=> nil
(let* ((default-directory default-directory))
default-directory)
=> "~/"
(if-let* ((a nil))
a
:else)
=> :else
(if-let* (a)
a
:else)
error=> Symbol's value as variable is void: a
(if-let* (default-directory)
default-directory
:else)
=> "~/"Note that build-in ...-let cannot support VARLIST that are just
SYMBOL. Doing so would conflict with the “one pair of parentheses can
be omitted if the VARLIST only contains one element” feature described
in the previous section.
(when-let (a b) ; binds one variable
(list a b))
macroexpand=>
(let* ((a (and t b)))
(if a
(list a b)))
(when-let* (a b) ; binds and references two variables
(list a b))
macroexpand=>
(let* ((a (and t a))
(b (and a b)))
(if b
(list a b)))Cond-Let’s implementations of ...-let* and ...-let require that all
elements of VARLIST have the form (SYMBOL VALUEFORM).
For both the build-in and Cond-Let’s implementations (of both
...-let and ...-let*) the canonical form of an element of VARLIST
is (SYMBOL VARFORM).
In the previous section we saw that for built-in ...-let* (but not for
...-let) an element can also be just SYMBOL. Additionally, for both
...-let* and ...-let, an element can be just (SYMBOL), i.e., VARFORM
can be omitted.
For the forms that support it (SYMBOL) and just SYMBOL behave the same
way, so refer to the previous section for details. Here we just add
some examples using (SYMBOL).
(when-let ((a))
a)
macroexpand=>
(let* ((s (and t a)))
(if s a))
(when-let* ((a))
a)
macroexpand=>
(let* ((s (and t a)))
(if s a))
Cond-Let’s implementations of ...-let* and ...-let require that all
elements of VARLIST have the form (SYMBOL VALUEFORM).
Sometimes we only want to check whether some VARFORM in VARLIST
returns non- nil, but do not want to also bind the value to any SYMBOL.
The built-in forms support two ways of doing that. Cond-Let’s implementations only support the second.
- An element of
VARLISTcan have the form(FORM). One special form of that,(SYMBOL), was already discussed above, butFORMdoesn’t actually have to beSYMBOL, it can also have the form(...).To determine which kind of
VARLISTentry one is dealing with, the (human) reader has to count parentheses. This is complicated by the fact that the wholeVARLISTadds another pair of parentheses. So if we are looking at((...on the first line of theVARLIST, that means something different than((...on another line. It is easy to miscount parens when writing or reading such forms.;; just SYMBOL (if-let* (a) a) macroexpand=> (let* ((a (and t a))) (if a a)) ;; ommitted VARFORM (if-let* ((a)) a) macroexpand=> (let* ((s (and t a))) (if s a)) ;; regular element (if-let* ((a 1)) a) macroexpand=> (let* ((a (and t 1))) (if a a)) ;; non-binding entry (if-let* (((a 1))) a) macroexpand=> (let* ((s (and t (a 1)))) (if s a))
- To avoid parentheses counting errors, one can use
_(or another symbol whose name begins with_). By using such a symbol one can inform the byte-compiler that some variable or argument is intentionally left unused. Without prefixing the symbol with an underscore, the byte-compiler would warn: Warning: argument `_’ not left unused. (This feature also benefits other forms, such aslambda,letandlet*.)So we might write
(when-let ((_ foo) (_ (foo bar)) (baz xxx)) ...)which leaves much less room for confusion.
However, for Emacs releases before 30.1, that results in Warning: argument `_’ not left unused, because the macro expansion does not actually leave
_unused.The fix for this can be backported for older releases like so:
(static-if (< emacs-major-version 30) (progn (defun internal--build-binding@backport-e680827e814 (fn binding prev-var) "Backport not warning about `_' not being left unused. Backport fix for https://debbugs.gnu.org/cgi/bugreport.cgi?bug=69108, from Emacs commit e680827e814e155cf79175d87ff7c6ee3a08b69a." (let ((binding (funcall fn binding prev-var))) (if (eq (car binding) '_) (cons (make-symbol "s") (cdr binding)) binding))) (advice-add 'internal--build-binding :around #'internal--build-binding@backport-e680827e814)))
(This backport does not bother with
_other-underscored-variables.)
As mentioned above Cond-Let’s implementations only support the second form, using a symbol whose name begins with an underscore. When using Cond-Let’s implementations, use of such a symbol does never result in such a warning due to macro expansion, regardless of what Emacs release is used.
Cond-Let’s and-let and and-let* take a single BODYFORM.
Built-in and-let* takes multiple BODY forms.
The progn below draw attention to the fact that there also is a
side-effect (in addition to the significant return value). Only the
second progn is mandatory.
(and-let* ((a (compute)))
(progn (side-effect a)
(modify a)))
(cond-let--and-let ((a (compute)))
(progn (side-effect a)
(modify a)))Note that before Emacs 27.1 the build-in and-let* behaved like
if-let*, not like when-let*, as had always been the intention.
Cond-Let’s when-let and when-let* do not allow an empty BODY. At
least one such form is required.
The built-in when-... allow an empty BODY. If the body is empty these
forms always return nil. (However, built-in if-... do not allow an
empty THEN, while still allowing ELSE to be empty. Cond-Let’s if-...
implementations work the same way as the built-in in this regard.)
While built-in if-... and when-... each come in pairs, a starred and
an unstarred variant, the same is not the case for and-.... Emacs
only implements and-let*.
Cond-Let’s implements both sides of all pairs, including the and-let /
and-let* pair.
While built-in if-... and when-... come in two variants, a starred and
a unstarred variant, the same is not the case for while-.... Emacs only
implements while-let (but it behaves like let*).
Another pair (and-...) is also not fully implemented in Emacs.
However, in that case the starred variant (and-let*) is implemented,
while in the case of while-... the unstarred variant (while-let)
exists.
Cond-Let’s implements both sides of all pairs, including the while-let
/ while-let* pair.