Hi, all. I recently ran into a situation where is case
would have been really helpful, and that prompted me to take a look at the history of this moderate proposal. It turns out it's been discussed [every] [year] [since] [Swift] [went] [open] [source], and that's just in the Evolution section.
Due to my RSI I can't actually be the one to implement it, but one thing I can contribute is a written-up proposal draft. Thoughts? Implementation volunteers? (And no, assume it's too late for this to make 5.7 even if everything comes together quickly.)
is case
: pattern-match boolean expressions
Introduction
Users regularly ask for an easy way to test if an enum matches a particular case, even if the enum is not Equatable, or if the case in question has payloads. A quick search of the forums turned up threads as old as 2015 asking about this---that is, about as old as the open source project itself.
Proposed solution
A new expression, <expr> is case <pattern>
, that evaluates to true
or false
based on whether <expr>
matches <pattern>
.
Detailed design
The Swift grammar gains a new expression production:
infix-expression -> is case <pattern>
The pattern must not have a trailing type-annotation
, and recursively must not contain a value-binding-pattern
(see Future Directions below).
<expr> is case <pattern>
should be considered equivalent to the following code:
({ () -> Bool in
switch <expr> {
case <pattern>: return true
default: return false
}
})()
In particular, the pattern may contain expressions matched with ~=
, just as in switch
and other pattern-matching constructs. The syntax is chosen to both imply a boolean output (is
) and evoke existing pattern-matching constructs (case
).
Some examples:
result is case .success(_)
result is case .failure(MyError.fileNotFound)
value is case (0..<limit)?
Precedence
By analogy with <expr> is <type>
, this expression should be usable within &&
/||
chains. That is, x && y is case .z && w
should be equivalent to x && (y is case .z) && w
. At the same time, other binary operators need to bind more tightly: x is case y ..< z
should be interpreted as x is case (y ..< z)
. This behavior is already implemented for chains of infix-expressions using precedence, but adding expression-patterns to the mix may be tricky to implement.
Open question: should
x ?? y is case .z
be treated asx ?? (y is case .z)
or(x ?? y) is case .z
? The former matchesis
's CastingPrecedence, designed aroundas?
, but the latter is still an option, and both have plausible uses:alwaysDark ?? (systemMode is case .dark)
vs(overriddenMode ?? systemMode) is case .dark
. The precedence ofis case
should be higher than ComparisonPrecedence no matter what, though.
If the pattern is known to always or never match the expression at compile time, the compiler should emit a warning. This includes "irrefutable" patterns that merely destructure their expression; these are not significantly different from type-casting patterns that are statically known to be upcasts, or values known to be out of range through constant propagation.
Source compatibility and ABI
This is an additive change to expression syntax that requires no additional runtime support; it has no source- or binary-compatibility implications beyond not being available in earlier versions of the compiler.
Alternatives considered
This is the longest section; it covers alternate syntaxes as well as proposals in similar spaces. I also put some Future Directions behind this fold.
Per-case optional properties
For years now there's been an idea that for case foo(bar: Int, baz: Int)
, the compiler could synthesize some or all of the following computed instance properties:
isFoo: Bool
asFoo: (bar: Int, baz: Int)?
bar: Int?
bar: Int
(if every case has a fieldbar: Int
)
This would handle the most common use for is case
, checking if a value with known enum type has a particular case. However, it does not cover all the use cases, such as matching nested values. Even if such a feature is proposed and accepted through the evolution process, is case
would still be useful.
Control-flow statements as expressions
If control-flow statements were expressions, you could implement this with if case <pattern> = <expr> { true } else { false }
, without having to wrap in a closure like my expansion above. However, this is still pretty verbose, and even Rust, which has generalized control-flow expressions, still provides a matches!
macro in its standard library.
case <pattern> = <expr>
There have been a handful of other proposed spellings over the years, most notably case <pattern> = <expr>
, by analogy with the existing if case
. However, while this syntax is not likely to be ambiguous in practice, it does suffer from the main flaw of if case
: the pattern comes first and therefore cannot be code-completed from the expression when typed left-to-right. The single =
also suggests assignment even though the result is a boolean.
<expr> case <pattern>
This is more concise, but would make it harder to parse switch statements:
doSomething()
case is UIButton // missing colon
doSomethingElse()
While this example is contrived, it shows how the compiler would have to jump through extra hoops to understand incomplete or erroneous code. So it's a good thing no one has seriously suggested this.
Special-case ==
or ~=
People like using ==
to compare non-payload cases, and ~=
is already used to match expression patterns. We could change the compiler to treat these differently from normal operators, allowing <expr> == <pattern>
or <pattern> ~= <expr>
. I'm personally not a fan of this, but I can't think of an inherent reason why it wouldn't work for enum cases. I'm hesitant to use ==
when other forms of matching are involved, but ~=
doesn't have that problem. It does, however, put the pattern on the left (established by existing implementations of the operator function), which again is sub-optimal for code completion. From a learning perspective, operators are also generally a bit harder to read and search for.
Change is
In theory, the existing cast-testing syntax <expr> is <type>
could be expanded to <expr> is <pattern>
, with <expr> is <type>
effectively becoming sugar for expr is (is <type>)
. This makes a very satisfying, compact syntax for pattern-matching as a boolean expression...but may add confusion around pattern matching in switch
statements, where case <type>
is disallowed, and case <type>.self
is an expression pattern. I don't think there's an actual conflict here, but only because of the requirement that types-as-values be adorned with .self
. Without that, case is <type>
would check runtime casting, but case <type>
would invoke custom expression matching, if an appropriate match operator is defined. (SE-0090 proposed to lift this restriction, but was deferred.)
Additionally, because there's an implementation of expression pattern matching that uses Equatable, we run into the risk of adding x is y
to the existing x == y
and x === y
. Having too many notions of equality makes the language harder to learn, as well as making it easier to accidentally pick the wrong one.
is case
sidesteps all these issues, and doesn't preclude shortening to plain is
later if we decide the upsides outweigh the downsides.
Wait for a Grand Unifying Pattern-Matching Proposal
There are a good handful of places where Swift's existing pattern-matching falls short, including if case
as discussed above, the verbosity of let
in patterns where case
is already present, the lack of destructuring support for structs and classes due to library evolution principles, and the inability for expression-matching to generate bindings. Proposals to address some or all of these issues, especially the last, might come with a new syntax for pattern matching that makes sense in and outside of flow control. Adding is case
does not help with these larger issues; it's only a convenience for a particular use case.
This is all true, and yet at the same time this feature has been proposed every year since Swift went open source (see the Acknowledgments below). If something else supersedes it in the future, that's all right; its existence will still have saved time and energy for many a developer.
Future Direction: Negation
This proposal provides a convenient way to check if an expression matches a pattern, but while that composably extends to !(<expr> is case <pattern>)
, the required parentheses feel clunky. Allowing <expr> is not case
or similar would help code read more smoothly.
Prefix and postfix operators not composing well is a concern for all existing binary operators, however (particularly as?
), so it may or may not be worth special-casing is case
.
Future Direction: Value Binding
The limitations on value binding come from a simple question: what happens to those bindings if the match fails? They have to be attached to a conditional scope, like the existing if case
, while case
, and guard case
syntaxes. Yet it's also been recognized that if case
is suboptimal, because the pattern comes first and therefore cannot be code-completed from the expression when typed left-to-right.
One could allow any of the following syntaxes for this:
if <expr> is case <pattern>
(downside: looks exactly like the boolean expression, but has additional powers due to its position)if <expr> as? case <pattern>
(downside: postfix?
usually implies Optional, but there's no Optional here)if <expr> as case <pattern>
(downside:as
in patterns does a test and a runtime cast, butas
in expressions is a compiler-inserted conversion, and this looks more like the latter than the former)if <expr> case <pattern>
(downside: doesn't parallelis case
as closely, but otherwise has few issues)if <expr> matches <pattern>
(downside:matches
isn't an existing keyword, so it's trickier to parse this)- (something else)
It's also worth noting that a hypothetical <expr> as! case <pattern>
would also allow for value bindings, answering the question of "what happens when match fails" with "abort". This operation has been requested as well, though not as frequently as is case
. However, for an arbitrary pattern containing value-binding this would be a statement rather than an expression (what type would the result have?). That would make it the first value-binding statement that doesn't start with a keyword. More thought should be given to possible syntaxes for a forcibly-applied pattern, or if it is more reasonable to solve this another way, possibly limiting support to enum cases and not arbitrary patterns.
Acknowledgments
- Andrew Bennett was the first person I could find suggesting the spelling
is case
for this operation, way back in 2015! - Alex Lew (2015), Sam Dods (2016), Tamas Lustyik (2017), Suyash Srijan (2018), Owen Voorhees (2019), Ilias Karim (2020), and Michael Long (2021) have brought up this "missing feature" in the past, often generating good discussion. (There may have been more that I missed as well, and this isn't even counting "Using Swift" threads!)
- Jon Hull (2018), among others, for related discussion on restructuring
if case
.