Shorthand for checking equality against multiple enum cases

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:

  1. This works, but feels a bit "heavy" because it requires creating an array just to do a check:
if [.active, .pending].contains(userStatus) { ... }
  1. A simple helper to Equatable types:
extension Equatable {
    func isAny(of cases: Self...) -> Bool {
        return cases.contains(self)
    }
}

// Usage
if userStatus.isAny(of: .active, .pending) { ... }

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.

3 Likes

Worth noting another existing option, though it is not shorter:

switch userStatus {
case .active, .pending:
  // do something
default: break
}
9 Likes

Another POV here might be to look at OptionSet as an alternative to enum.

3 Likes

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.

2 Likes

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

What should be:

(.active | .pending).contains(userStatus)
(.active, .pending).contains(userStatus)
(.active, .pending) ~= userStatus

The meanings of these are identical if UserStatus is an OptionSet.

The closest you can currently get:

(UserStatus.active | .pending).contains(userStatus)
contains((UserStatus.active, UserStaus.pending), userStatus)
(UserStaus.active, UserStaus.pending) ~= userStatus
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
}
1 Like

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]

infix operator =~: ComparisonPrecedence

extension Equatable {
    static func | (lhs: Self, rhs: Self) -> [Self] {
        return [lhs, rhs]
    }

    static func | (lhs: [Self], rhs: Self) -> [Self] {
        return lhs + [rhs]
    }

    static func =~(lhs: Self, rhs: [Self]) -> Bool {
        return rhs.contains(lhs)
    }
}

if userStatus =~ .active | .pending { 
    // Do something
}
(Performance)

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


  1. 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. ↩︎

4 Likes

Using a macro from something like GitHub - griotspeak/DiscriminatedUnion: Swift macro for Discriminated Unions (other people have implemented the same idea… I tried to find the example that was in swift-macro but failed. Becca might have implemented it?) makes this less cumbersome for a definition of ‘less cumbersome’ that discounts writing the macro.

In any case, I’ve just added hasDiscriminant so one could write

if userStatus.hasDiscriminant(in: [.active, .pending]) { 
    // Do something
}

I’m not sure if I endorse using DiscriminatedUnion as your dependency but I also do use it in almost all of my repos now so… :shrug:

As long as your enum is Equatable, this is the best approach, IMO. I wouldn’t worry about the array being “heavy”. A array of constants like this is optimized down to almost nothing.

10 Likes

I must be missing something, because this doesn’t work for me:

enum Foo: Equatable {
    case bar(Int)
    case baz(String)
    case quux
}

func f(arg: Foo) {
    if [.bar, .baz].contains(arg) {
        // error: instance method 'contains' requires that 'Foo' conform to 'Collection'
        print("hello")
    }
}
// bad guess

That’s what I get for the quick try. Though I am surprised that it works without the fully specifying the first since there are at least a few cases where the compiler has not done that for me

Are you saying that this code compiles for you? Because it doesn’t for me. Which makes sense, because adding Foo. shouldn’t change any semantics; it should just guide the type checker.

Edit: OK, the problem is the associated values. If I remove them, then the expression [.bar, .baz].contains(arg) compiles. I don’t know why I thought that @marlonjames71’s original use case had associated values.

OK, with that distraction out of the way, I can make the comparison I originally wanted to do.

Restating the example, I’ve removed the print() statement and written a version that uses switch:

public func disassembleMe(arg: Foo) -> Bool {
#if true
	return [.bar, .baz].contains(arg)
#else
	switch arg {
	case .bar:
		fallthrough
	case .baz:
		return true
	default:
		return false
	}
#endif
}

Here’s what it compiles down into (swift build -c release):

_$s12discriminant13disassembleMe3argSbAA3FooO_tF:
00000000000018c4	stp	x20, x19, [sp, #-0x20]!
00000000000018c8	stp	x29, x30, [sp, #0x10]
00000000000018cc	add	x29, sp, #0x10
00000000000018d0	and	w19, w0, #0xff
00000000000018d4	adrp	x0, 7 ; 0x8000
00000000000018d8	add	x0, x0, #0xc8
00000000000018dc	bl	___swift_instantiateConcreteTypeFromMangledName
00000000000018e0	adrp	x1, 7 ; 0x8000
00000000000018e4	add	x1, x1, #0xa0
00000000000018e8	bl	0x1be8 ; symbol stub for: _swift_initStaticObject
00000000000018ec	ldr	x8, [x0, #0x10]
00000000000018f0	add	x9, x0, #0x20
00000000000018f4	mov	x10, x8
00000000000018f8	subs	x8, x8, #0x1
00000000000018fc	b.lo	0x190c
0000000000001900	ldrb	w11, [x9], #0x1
0000000000001904	cmp	w11, w19
0000000000001908	b.ne	0x18f4
000000000000190c	cmp	x10, #0x0
0000000000001910	cset	w0, ne
0000000000001914	ldp	x29, x30, [sp, #0x10]
0000000000001918	ldp	x20, x19, [sp], #0x20
000000000000191c	ret

That seems pretty heavy for a simple discriminant check. It instantiates type metadata, creates an array object, and appears to iterate over the 2 members of the array to compare them against arg.

If I swap the #if condition to compile the version that uses switch, it is drastically simpler:

_$s12discriminant13disassembleMe3argSbAA3FooO_tF:
00000000000018c4	and	w8, w0, #0xff
00000000000018c8	cmp	w8, #0x2
00000000000018cc	cset	w0, lo
00000000000018d0	ret

I think this is a pretty clear reason to avoid [.foo, .bar].contains(.blah), especially in an inner loop.

5 Likes

A macro on a case identification or discriminanti type still solves the problem you are pointing out though, right? The macro couldn’t use contains but its still straightforward “generate the switch”

Yeah, I would hope that a macro would be able to produce identical codegen to the manually written switch (and maybe even handle cases with associated values!). I haven’t tried yours yet to confirm; I was just comparing what can be done with the language “out of the box.”

The quick thing I added used a set of the discriminants so I think it will still be ‘heavy’ by the standard you just set checking compilation but I’ll probably try generating a switch in the next couple days just to see what that would entail.

1 Like

I personally really like the way C# implemented pattern matching.
With it we could do the following instead:

if userStatus is .active or .pending { 
    // Do something
}

Implementing this in Swift's syntax roughly means:

  • extend expr with is defined as <expr> is <pattern>
  • extend pattern with and defined as <pattern> and <pattern>
  • extend pattern with or defined as <pattern> or <pattern>.
#hasDiscriminant(userStatus, in: .active, .pending)

Implementation note: This… this is weird and I don’t know how much of the weird is because I am playing fast and loose writing this before going to bed and how much cannot be avoided with the restrictions on macros.

I couldn’t hang the method off of a type anymore so I made a freestanding macro. This made some sense once I realized that each call needs to generate a switch based on the provided cases. I don’t remember how to create a unique identifier for the function body that I would essentially make so I just made a closure and called it in place.

All of which is to say that I don’t know if the closure will add overhead.

1 Like

Good news! After tweaking the DiscriminatedUnionClient code slightly to prevent constant folding or dead code stripping…

@discriminatedUnion
public enum Pet {
    case dog
    case cat(curious: Bool)
    case parrot(loud: Bool)
    case snake
    case turtle(snapper: Bool)
//    case bird(name: String, Int)

}

public func disassembleMe(arg: Pet) -> Bool {
#if true
    return #hasDiscriminant(arg, in: .dog, .cat)
#else
    switch arg {
    case .dog:
        fallthrough
    case .cat(_):
        return true
    default:
        return false
    }
#endif
}

…both the macro and the hand-written switch produce identical machine code:

_$s24DiscriminatedUnionClient13disassembleMe3argSbAA3PetO_tF:
0000000000000da8	ubfx	w8, w0, #6, #2
0000000000000dac	cbz	w8, 0xdc4
0000000000000db0	cmp	w8, #0x3
0000000000000db4	b.ne	0xdcc
0000000000000db8	and	w8, w0, #0xff
0000000000000dbc	cmp	w8, #0xc0
0000000000000dc0	b.ne	0xdcc
0000000000000dc4	mov	w0, #0x1
0000000000000dc8	ret
0000000000000dcc	mov	w0, #0x0
0000000000000dd0	ret
2 Likes

It is possible (see below). You'd need brackets as otherwise the expression would be considered as: (userStatus == .active) || .pending.

As a side note: <= seems to be more appropriate.

Here's my take. Setup:

protocol P {}

indirect enum IndirectOptional<Wrapped> {
    case some(Wrapped)
    case none
}

struct Node<T> {
    var payload: T
    var next: IndirectOptional<Node<T>>
}

extension P {
    static func || (lhs: Self, rhs: Self) -> Node<Self> {
        Node(payload: lhs, next: .some(Node(payload: rhs, next: .none)))
    }
    static func || (lhs: Self, rhs: Node<Self>) -> Node<Self> {
        Node(payload: lhs, next: .some(rhs))
    }
    static func || (lhs: Node<Self>, rhs: Self) -> Node<Self> {
        rhs || lhs // ignore the order for now
    }
}
func == <T: P & Equatable> (lhs: T, rhs: Node<T>) -> Bool {
    var rhs = rhs
    while true {
        if lhs == rhs.payload { return true }
        switch rhs.next {
            case .none: return false
            case .some(let node): rhs = node
        }
    }
}

Test:

enum E: P {
    case active, pending, other
}

func test(userStatus: E) {
    if userStatus == (.active || .pending) { 
        print("contains")
    } else {
        print("does not contain")
    }
}

test(userStatus: .active)

Just be warned that this sort of thing can easily get you into trouble by slowing down the type checking of other seemingly-unrelated expressions in the same module (or other modules, if the overloads were public) that happen to involve || and ==, even if they only use those operators in the existing way.

3 Likes