This package provides two macros, ucond
and ucase
,
which extend the built-in cond
and pcase
with more flexible control flows:
-
Mutually nested and interleaved
ucond
anducase
, where inner constructs can fall through to the outer level.(ucase (list 1 2 3 4) ; Start a pattern matching like pcase. (`(1 2 . ,rest) ; Matched, then :and-ucase (length rest) ; start a nested pattern matching. (0 'error-1) ; Not matched. Move next. (1 'error-2)) ; Not matched. Fall through to outer ucase. (`(1 . ,_) 'yes)) ; Match. Return 'yes.
-
Interleaved
let*
, which works likepcase-let*
with an otherwise branch and an option to fall through to the outer level.(ucond ; Start a multi-way if like cond. (let* ((x 1) (y (1+ x)))) ; Bind x and y. ((> x y) 'error-1) (let* (`(,a ,b ,c ,d) (list x y)) ; Failed to bind a, b, c, d, :otherwise (error-2 x y)) ; return the :otherwise branch. ((< x y) 0))
The primary motivation for ucond
and ucase
is to flatten nested code
while maintaining or even improving the program's readability and clarity.
Here are some quick comparisons to see how ucond
and ucase
flatten programs.
-
Before: Nested
pcase
andcond
for destructuring bindings and comprehensive error handling.(pcase (get-a-pair) (`(,x1 . ,x2) (pcase (get-status x1 x2) (`(:status 200 :data ,x3) (cond ((test-a x1 x2 x3) 'a) ((test-b x1) 'b) (t 'error-3))) (_ 'error-2))) ; Error handling far away from pattern, (_ 'error-1)) ; and written in reversed order.
After: Flattened by
ucond
and interleavedlet*
with early returns.(ucond (let* ((`(,x1 . ,x2) (get-a-pair))) :otherwise 'error-1) (let* ((`(:status 200 :data ,x3) (get-status x1 x2))) :otherwise 'error-2) ((test-a x1 x2 x3) 'a) ((test-b x1) 'b) (t 'error-3))
-
Before: Nested
pcase
matching the same expression to delay a heavy computation.(pcase (get-response) (`(:failure ,err) (report-error err)) (`(:simple ,x) x) (response (let ((num (heavy-computation))) ; Compute the pattern. (pcase response ; Continue matching. (`(:ok ,(pred (< num)) ,x) x) ; Pattern used here (`(:double ,(pred (> num)) ,x) (* 2 x)) ; and here. (_ 'error-a)))))
After: Flattened by
ucase
with interleavedlet*
.(ucase (get-response) (`(:failure ,err) (report-error err)) (`(:simple ,x) x) (let* ((num (heavy-computation)))) (`(:ok ,(pred (< num)) ,x) x) (`(:double ,(pred (> num)) ,x) (* 2 x)) (_ 'error-a))
-
Before: Nested
pcase
and a local function to handle common fallback logic.(cl-flet ((fallback () (verbose-fallback) (clean-up) (report-error))) (pcase expr1 (`(ok ,x1) (pcase (compute-something x1) (`(case1 ,x2) (f1 x1 x2)) (`(case2 ,x2 ,x3) (f2 x1 x2 x3)) (_ (fallback)))) ; Cannot jump to the next clause (_ (fallback)))) ; in the outer pcase directly.
After: Simplified by nested
ucase
and:and-ucase
with fall-through.(ucase expr1 (`(ok ,x1) :and-ucase (compute-something x1) (`(case1 ,x2) (f1 x1 x2)) (`(case2 ,x2 ,x3) (f2 x1 x2 x3))) (_ (verbose-fallback) (clean-up) (report-error)))
Warning
This package is still in alpha stage and is subject to major syntax changes. Do not use it in production until it is on MELPA.
use-package
with package-vc
:
(use-package ucond
:vc (:url "https://github.com/hawnzug/ucond.git" :rev :newest))
use-package
with elpaca
:
(use-package ucond
:ensure (:host github :repo "hawnzug/ucond"))
Or manually download ucond.el
, add it to your load-path
, and (require 'ucond)
.
The macros ucond
and ucase
can be used as drop-in replacements
for the built-in cond
and pcase
in most situations:
(ucond
(condition-1 body-1...)
(condition-2 body-2...)
(condition-3 body-3...))
(ucase expr
(pattern-1 body-1...)
(pattern-2 body-2...)
(pattern-3 body-3...))
In addition to default clauses like (condition body...)
and (pattern body...)
,
ucond
and ucase
also support special clauses like (let* ...)
to allow for more flexible control flows.
In this quick start, we will introduce several commonly used special clauses.
We can add local bindings between clauses with the special let*
clause:
(ucond
(let* ((x 1) (y 2)))
(condition-1 (+ x y))
(let* ((z (+ x y)) (w (1+ z))))
(condition-2 (+ x y z w)))
(ucase expr
(let* ((x 1) (y 2)))
(pattern-1 (+ x y))
(let* ((z (+ x y)) (w (1+ z))))
(pattern-2 (+ x y z w)))
Most special clauses like let*
work the same in both ucond
and ucase
.
Therefore, the rest of this quick start will only show examples in ucond
.
The let*
clause is not the built-in Elisp special form.
It is overloaded in ucond
and ucase
but has a similar syntax.
The let*
clause actually works more like pcase-let*
,
so we can do pattern matching and destructuring bindings within it:
(ucond
(let* ((`(,x ,y ,z) (list 1 2 3))))
((< x y z) (+ x y z))) ; => Returns 6
When pattern matching fails in a let*
clause,
we can use the :otherwise
keyword to specify the return value:
(ucond
(let* ((`(,x ,y) (list 1 2 3)))
:otherwise 42) ; => Returns 42
(t (+ x y)))
We can introduce nested and interleaved ucond
and ucase
using the special ucond
and ucase
clauses:
(ucond
(nil 0)
(ucase (+ 1 1)
(0 1)
(2 2)) ; => Returns 2
(t 3))
When all the clauses are exhausted in an inner ucond
or ucase
,
the control flow will fall through to the next clause in the outer level:
(ucond
(nil 0)
(ucase (+ 1 1)
(0 1)
(100 2)) ; Fall through
(nil -1) ; Examined
(t 3)) ; => Returns 3
The let*
clause can also lead to fall-through
when pattern matching fails and there is no :otherwise
branch:
(ucond
(nil 0)
(ucase (+ 1 1)
(let* ((`(,x ,y) (list 1 2 3)))) ; Fall through
(_ (+ x y))) ; Skipped
(t 3)) ; => Returns 3
If a fall-through occurs in the outermost level,
the entire construct returns nil
:
(ucond (nil 1)) ; Fall through
; Returns nil
(ucond
(let* ((`(,x ,y) (list 1 2 3)))) ; Fall through
(t (+ x y)))
; Returns nil
This behavior coincides with cond
and pcase
when their clauses are exhausted.
Nested ucond
and ucase
can also be introduced with the
:and-ucond
and :and-ucase
keywords,
immediately after a condition in ucond
or a pattern in ucase
:
(ucond
(condition-1
:and-ucond
(condition-2 body-1)
(condition-3
:and-ucase expr-1
(pattern-1 body-2)
(pattern-2
:and-ucond
(condition-4 body-3)))))
They work like a directly nested ucond
(or ucase
)
with a pre-condition (or pre-pattern),
and the fall-through behavior is the same.
These examples should give you a feel for how to use ucond
and ucase
in practice.
If you just need a quick reference, see the built-in docstrings for ucond
and ucase
.
For a deep dive into the core mechanics, read the Complete Guide below.
As shown in the examples above, ucond
and ucase
look like the built-in constructs cond
and pcase
:
(ucond CLAUSE...)
(ucase EXPR CLAUSE...)
The key difference is that
there are special clauses which can introduce nested ucond
and ucase
,
forming a single tree-like conditional structure.
For example:
(ucond ; A nesting can be introduced by:
(ucase EXPR CLAUSE...) ; the ucase clause,
(CONDITION :and-ucase EXPR CLAUSE...) ; the :and-ucase keyword,
(ucond CLAUSE...) ; the ucond clause,
(CONDITION :and-ucond CLAUSE...)) ; the :and-ucond keyword.
It is important to distinguish this single nested tree from a standard nested form:
(ucond
(CONDITION (ucond CLAUSE...))) ; This is not ucond's nesting.
This is not a single tree but two separate ucond
.
The inner ucond
is the body of the CONDITION
clause, not a sub-clause of the outer ucond
.
Their macro expansions are independent.
There are three basic control-flow rules of ucond
and ucase
:
- Top-to-Bottom: Just like
cond
andpcase
, clauses are executed sequentially. - First-Success: Only the first clause that successfully runs its body will return a value, which becomes the value of the entire top-level construct.
- Fall-Through: When the control flow reaches the end of an inner block,
it falls through to the next clause in the parent block.
If it is alreadly at the outmost block, the entire construct evaluates to
nil
.
The first two rules are the same in cond
and pcase
.
The third "Fall Through" rule is a natural generalization of the "Top-to-Bottom" execution in a nested setting.
All the clauses available in ucond
and ucase
can be built upon two primitives,
let*
and match*
, which provide opposite control flows.
The let*
clause works like a guard.
The subsequent clauses will only be executed when pattern matchings in let*
succeed.
Syntax: (let* ((PATTERN EXPR)...) [:otherwise BODY...])
, where the :otherwise ...
branch is optional.
- If all patterns match, the control flow moves to the next clause,
and the bindings are made available to all subsequent clauses
within the current
ucond
orucase
. - If any pattern fails to match:
- If the
:otherwise
branch is non-empty, return the value ofBODY...
. - If there is no
:otherwise
branch, orBODY
is empty, end the currentucond
orucase
and fall through to the parent block.
- If the
The match*
clause works like a standard case in pcase
,
which executes its body when the pattern matchings succeed.
Syntax: (match* ((PATTERN EXPR)...) BODY...)
.
- If any pattern fails to match, the control flow moves to the next clause after
match*
. - If all patterns match, make the bindings available to
BODY...
and run it.BODY...
has three variations:- If
BODY...
is:and-ucond CLAUSES...
, start a nesteducond
withCLAUSES
. - If
BODY...
is:and-ucase EXPR-1 CLAUSES...
, start a nesteducase
with the expressionEXPR-1
andCLAUSES
. - Otherwise, return
BODY...
.
- If
The flexibility of ucond
and ucase
comes from combining these opposite behaviors.
Condition | Control Flow of let* |
Control Flow of match* |
---|---|---|
Patterns Match | To next clause (with new bindings) | To body (with new bindings) |
Patterns Fail | To :otherwise (or fall through) |
To next clause |
All other clauses can be derived from either let*
or match*
.
Clauses based on let*
(OTHERWISE
represents the optional :otherwise ELSE...
branch):
(when-let* ((VAR EXPR)...) OTHERWISE)
: Alet*
that also checks the bound variables are non-nil. Analog to the built-inwhen-let*
.- Equivalent to
(let* (((and VAR (guard VAR)) EXPR)...) OTHERWISE)
.
- Equivalent to
(when CONDITION OTHERWISE)
: Alet*
that only checks one condition is non-nil. Analog to the built-inwhen
.- Equivalent to
(let* (((guard CONDITION) t)) OTHERWISE)
.
- Equivalent to
Clauses based on match*
(BODY...
can begin with :and-ucond
or :and-ucase
to start nesting):
(ucond CLAUSES)
: Starts a nesteducond
block.- Equivalent to
(match* ((_ t)) :and-ucond CLAUSES)
.
- Equivalent to
(ucase EXPR CLAUSES)
: Starts a nesteducase
block.- Equivalent to
(match* ((_ t)) :and-ucase EXPR CLAUSES)
.
- Equivalent to
- In
ucond
,(CONDITION BODY...)
is the standardcond
-like clause.- Equivalent to
(match* (((guard CONDITION) t)) BODY...)
. - When
BODY...
is empty, as in(CONDITION)
, the value ofCONDITION
is returned (only evaluated once). This is for compatibility withcond
.
- Equivalent to
- In
ucase
,(PATTERN BODY...)
is the standardpcase
-like clause.- Equivalent to
(match* ((PATTERN VAL-EXPR)) BODY...)
, whereVAL-EXPR
is the value of the expression matched byucase
, that is, the first argument ofucase
.
- Equivalent to
Below is a table summarizing all derivable clauses and their equivalent forms in let*
or match*
:
Derivable Clause | Equivalent Form in Primitive Clause |
---|---|
(when-let* ((VAR EXPR)...) OTHERWISE) |
(let* (((and VAR (guard VAR)) EXPR)...) OTHERWISE) |
(when CONDITION OTHERWISE) |
(let* (((guard CONDITION) t)) OTHERWISE) |
(ucond CLAUSES) |
(match* ((_ t)) :and-ucond CLAUSES) |
(ucase EXPR CLAUSES) |
(match* ((_ t)) :and-ucase EXPR CLAUSES) |
(PATTERN BODY...) |
(match* ((PATTERN EXPR)) BODY...) |
(CONDITION BODY...) |
(match* (((guard CONDITION) t)) BODY...) |
(CONDITION) |
(match* (((and x (guard x)) CONDITION)) x) |
Note: The equivalent forms are only shown to clarify their semantics. The actual implementation might differ.
This section is intended for readers familiar with other languages,
and want to relate ucond
and ucase
to existing language features.
It is not necessary to read this section to learn how this package works,
and it might be full of obscure terminologies specific to particular languages.
The most distinctive feature of ucond
and ucase
is the interleaved (let* ... :otherwise ...)
construct within multi-way if (cond
) and pattern matching.
At first glance, this construct is similar to the let-else
in Rust
and the guard-let-else
in Swift.
But they differ in important ways.
In Rust and Swift,
these constructs are only used within a sequential construct,
that is, a sequence of statements.
They are not interleaved in a branching construct like multi-way if or pattern matching.
Therefore their else
blocks must always diverge, for example, return, break, or error.
On the other hand, the let* :otherwise
clause in this package is used in a branching construct,
so it allows the :otherwise
branch to contain any expressions or fall through.
It can be viewed as a generalization of let-else
in a nested branching construct.
Without :otherwise
in let*
, this package is almost the same as
The Ultimate Conditional Syntax in MLscript,
if we ignore its ML-like and indentation sensitive syntax and focus on the abstract syntax.
In fact, this paper is a major inspiration of this package.
The nested and interleaved ucond
and ucase
with fall-throughs can be viewed as the nested guards in Haskell
(though not yet implemented), see
these
three
questions for examples.
Note that Haskell's chained guards and interleaved let-bindings in guards
are different from nested guards and interleaved let*
between clauses.
OCaml does not support nested guards, but there is an extensive
discussion about it,
proposing a syntax similar to ucase
.
Scala also has a similar proposal for nested patterns in guards.
Agda's with
is a clean syntax (but it's more than that)
for nested guards (on the left), but it has no fall-through behavior.
Racket does not support nested guards directly,
but can easily implement this feature using labelled branches (=> id
)
and manually escaping to labels.
It offers more freedom and works like a goto in a pattern matching.
This feature is also listed as a todo in the source code of pcase
.
Emacs 31 added a new cond*
construct which extends the traditional cond
.
It supports "non-exit clause" which works similar to the let*
clause in this package,
but it is non-exiting, so the control flow is more limited.
There is no nested guards with fall-through in cond*
.
The match*
keyword in this package is borrowed from cond*
, but they have different behaviors.
The built-in pcase
does not support interleaved variable bindings like let*
.
However, it is actually possible to have nested guards with fall-through using the or
pattern.
For example, the following nested ucase
(ucase (list 1 2 3 4)
(`(1 . ,rest)
:and-ucase (reverse rest)
(`(2 ,x ,y) (+ x y))
(`(3 ,z) (* z z)))
(_ 'fallthrough))
is equivalent to this single pcase
:
(pcase (list 1 2 3 4)
(`(1 . ,(app reverse
(or (and `(2 ,x ,y) (let ret (+ x y)))
(and `(3 ,z) (let ret (* z z))))))
ret)
(_ 'fallthrough))
Although it is more verbose, the logic still looks clear.
Scalability (more nesting and more branches) might be an issue,
and using the or
pattern in this way has the problem of making all bindings available to the body.
- Performance might degrade with deeply nested
ucond
anducase
that could fall through, roughly oneif
andeq
per level, compared to a directgoto
, which is not available in Elisp. - In some cases,
cond
might be compiled to a jump table, but a direct translation toucond
might not be, which would makeucond
slower. - Some Emacs keywords like
let*
andwhen
are overloaded inucond
anducase
, which might lead to confusion. Keywords like:otherwise
might seem unfamiliar to Elisp users. The byte-compiler might warn about aThis package should no longer generate shadowed patterns. If you see such warnings and there are no shadowed patterns in your code, please open an issue.pcase
pattern being shadowed. This will not affect correctness, but the issue should be fixed if possible.
This package does not provide:
-
A new pattern language. It uses
pcase
under the hood. -
A loop construct. It is not
cl-loop
and has nothing to do with loops. -
A new sequential construct. Ultimately,
ucond
anducase
are conditional constructs that form a tree-like structure, and they will choose one branch to execute. It might be tempting to define a macro likeucond-defun
:(ucond-defun func (args...) "Docstring" (let* ... :otherwise ...) (let* ... :otherwise ...) (ucase ...) (ucond ...) BODY...)
Although this macro looks convenient, it is not recommended and is considered an abuse of
ucond
.
- The Ultimate Conditional Syntax:
The 'u' in
ucond
anducase
comes from this paper. This package is basically the ultimate conditional syntax, plus the special(let* ... :otherwise ...)
clause. - "... We all want it, ...", an answer to the StackOverflow question "Is it possible to nest guards in Haskell?". Although I'm not sure if "we" really all want it, at least I do.
- Rust
let else
RFC: It seems that many Rust users like this feature. - Tail cascade: a new indentation style for some OCaml constructs: This post advocates for a style that linearizes cascading tail pattern-matchings to avoid cascading indentations.
- Elisp built-in
pcase
: This is whatucond
anducase
expand to. - Elisp built-in
cond*
: It inspired me to unify the syntax ofucond
anducase
. The keywordmatch*
also comes fromcond*
.