I’ve noticed that I often need to check if a value matches one of several enum cases. While Swift is generally very expressive, this specific pattern feels a bit repetitive. I’m curious if there has ever been a discussion about making this more concise.
In a perfect world, I’d love to be able to write something like this:
if userStatus == .active || .pending {
// Do something
}
Currently, we have to repeat the variable name (userStatus == .active || userStatus == .pending), which can make if statements quite long as the variable names or the number of cases grow, and less ergonomic I guess.
I’m curious if this type of comparison has been considered before.
Is there a fundamental reason why || couldn't be used this way (e.g., how the compiler reads logic)?
Are there plans for "pattern matching" improvements that might cover this?
I know the syntax above isn't possible right now, so I’ve been using two alternatives:
This works, but feels a bit "heavy" because it requires creating an array just to do a check:
if [.active, .pending].contains(userStatus) { ... }
I personally feel the extension method reads very well, but I’d love to hear the community’s thoughts on whether a native language change (like my first example) would be a nice addition to Swift.
OptionSets are sort of a weird "we needed a way to bridge C bitfield enums and Swift values" and I'm not sure I'd recommend designing entirely new types to use it. The limitation that you can only have as many values as the number of bits in the integer type you use as your raw type makes it awkward to use.
And in the situation cited, the two cases appear to be mutually exclusive: .active and .pending are separate states and you wouldn't want [.active, .pending] to be something the system can represent as a legitimate value.
An OptionSet type always represents a collection. Conflating single options and collections of them by having the enclosing brace syntax [.active] on static properties to mean the same thing as the non-braced version should never have been allowed. That should only mean Array<Options>.
That's the way I do it, but it would be better to replace the [] with () via same-type parameter packs, tuple extensions, and better type inference (so you don't need to use the name of the enum everywhere, here):
extension OptionSet {
static func | (set0: Self, set1: Self) -> Self {
set0.union(set1)
}
}
func contains<each Element, Match: Equatable>(
_ element: (repeat each Element),
_ match: Match
) -> Bool {
for case let element as Match in repeat each element {
if element == match { return true }
}
return false
}
func ~= <each Element, Match: Equatable>(
_ element: (repeat each Element),
_ match: Match
) -> Bool {
for case let element as Match in repeat each element {
if element == match { return true }
}
return false
}
I agree. In many natural languages, like English, relational operators distribute over boolean operators: the sentence "the user status is active or pending" expands to "(the user status is active) or (the user status is pending)". It would be nice if more programming languages worked like that.
In the meantime, you can get close with custom operators:[1]
From some experimentation, it looks like we can get the compiler to generate more efficient code by using array literals instead of the | operator, using a manual reimplementation of Array.contains, and adding the @inline(always) attribute to the implementation of =~. The @inline(always) attribute was introduced by SE-0496, but isn't released yet, so for now we have to use the compiler-private spelling, @inline(__always).
infix operator =~: ComparisonPrecedence
extension Equatable {
@inline(__always)
static func =~(lhs: Self, rhs: [Self]) -> Bool {
for i in rhs {
if lhs == i {
return true
}
}
return false
}
}
enum UserStatus {
case active, pending, somethingElse
}
func f(userStatus: UserStatus) -> Bool {
return userStatus =~ [.active, .pending]
}
The compiler (aarch64 swiftc 6.2) with -O optimizes f to the following: (Compiler Explorer)
output.f(userStatus: output.UserStatus) -> Swift.Bool:
and w8, w0, #0xff
cmp w8, #2
cset w0, lo
ret
The optimization also happens with userStatus =~ .active | .pending, using the same | implementation (Compiler Explorer). But the optimization breaks with more than two operands to the | operator, like userStatus =~ .active | .pending | .somethingElse, even with various optimizations to the | implementation (Compiler Explorer, Compiler Explorer). The optimization still works with more than two operands to an array literal, like userStatus =~ [.active, .pending, .somethingElse] (Compiler Explorer).
It's also possible to use InlineArray instead of Array, which results in the same optimization even without @inline(always) (Compiler Explorer). But that makes it impossible to implement the | operator for more than two operands, because it's impossible to use arithmetic for the length of an InlineArray, like [n+1 of Int].
A problem with the || operator specifically is that it has lower precedence than ==. The compiler does parsing before type checking, so when it parses userStatus == .active || .pending, it can't distinguish .pending from a boolean value like list.isEmpty.
The most recent information I'm aware of is that a few years ago, the Language Steering Group decided they would hold on small pattern matching improvements until there's a more cohesive vision for the future of pattern matching in Swift. [Pitch] is case expressions - #69 by John_McCall
I chose =~ instead of the existing ~= operator (which can customize switch statements) because with ~=, the pattern is supposed to go on the left instead of the right, like .active | .pending ~= userStatus. ↩︎