Skip to content

Differences

Jonas Bernoulli edited this page Aug 19, 2025 · 1 revision

Differences between built-in and Cond-Let’s implementations

(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-let means ”if-let, when-let, and-let and/or while-let”.
  • X-let* means ”if-let*, when-let*, and-let* and/or while-let*”.
  • if-... means ”if-let and if-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).

Emacs’s non-star implementations behave like 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 the VALUEFORMs are evalled before any symbols are bound.
  • let*: Each VALUEFORM can refer to the symbols already bound by VARLIST.
;; `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]

Cond-Let’s if-let and if-let limit extend of bindings

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)

Built-in if-let and when-let have a single binding special case

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*.

Built-in ...-let* allow VARFORM entries that are just SYMBOL

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).

Built-in ...-let* allow omitting VARFORM

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).

For older Emacs releases use of _ results in a warning

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.

  1. An element of VARLIST can have the form (FORM). One special form of that, (SYMBOL), was already discussed above, but FORM doesn’t actually have to be SYMBOL, it can also have the form (...).

    To determine which kind of VARLIST entry one is dealing with, the (human) reader has to count parentheses. This is complicated by the fact that the whole VARLIST adds another pair of parentheses. So if we are looking at ((... on the first line of the VARLIST, 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))
        
  2. 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 as lambda, let and let*.)

    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

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 omitting BODY

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.)

Emacs does not implement and-let

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.

Emacs does not implement while-let*

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.