Shorthand for checking equality against multiple enum cases

IMO, this feels like code golf. A switch is a perfectly idiomatic way to check multiple cases and is considerably more readable than the patterns that try to rig things up with operators to make it read more like English.

16 Likes

Sounds like there’s a missed optimization somewhere, might be worth filing a bug. I tried this test case with Swift 6.2:

public enum E {
  case w
  case x
  case y
  case z
}

public func testFunction(e: E) -> Bool {
  return [.x, .y].contains(e)
}

If I compile it with -S -O, I get the following:

_$s1e12testFunctionAASbAA1EO_tF:
	and	w8, w0, #0xff
	sub	w8, w8, #1
	cmp	w8, #2
	cset	w0, lo
	ret

If I change the contents of the array to include other subsets of the cases it gets optimized down in other clever ways.

1 Like

Personally, I view getting it down to operator syntax a non goal but switching over instances that have associated value before you have an instance is an actual sticking point.

Writing a state machine is the most straightforward example I have.

1 Like

Swift 6.2 on godbolt shows a more elaborate asm for me for that fragment. I could see the optimised version you are talking about on the nightly version on godbolt.

How is this optimisation done, is it specific to array and won't work with a dictionary or a set?

I would tweak isAny as follows:

enum E
{
   case active
   case pending
   case inactive
}

extension Equatable 
{
    @inline( __always )
    func isOne(of candidates: Self...) -> Bool
    {
        candidates.contains(self)
    }
}

@main
class MainProgram
{
   static func main()
   {
      let varTypeE :E = .active
      
      if varTypeE.isOne(of: .active, .pending )
      {
          print( "It matched" )
      }
   }
}
3 Likes

Sorry I'm just replying now. Was on vacation and then getting back into the flow of work again. Some really great insights have been shared and I'm appreciative for all the responses.

I agree, this is probably the best out of box way to go. Meant to mention this in my post.

This is really nice and reads so clean and simple!

That's super clean. Also, I don't think I've personally used @inline before.

That's pretty dang close to my original want. That's awesome. I totally forgot about indirect enum. Thank you!

That makes sense. I think I meant that it feels heavy syntax-wise.

1 Like

Your answer is super insightful. Thanks a ton! That does make a lot of sense. I'm not too imaginative right now, but I wonder what breaks (I imagine a lot) if || got a higher precedence to make that work.

1 Like

Looking at Rust as another language with very similar enums and pattern matching, they resolve this kind of problem with a macro:

enum Foo {
    Bar(i32),
    Baz(String),
    Quux,
}

fn f(arg: Foo) {
    if matches!(arg, Foo::Bar(_) | Foo::Baz(_)) {
        println!("hello");
    }
}

The matches macro just gets converted into a match expression (similar to Swift’s switch statement).

Swift could probably provide a very similar macro for this use case.

EDIT:
Thinking more about this, the obvious blocker here is that while Rust macros take in raw syntax, meaning they can just take patterns like this, Swift macros cannot do that AFAIK. It would be nice if Swift had a way to define syntactical only macros (a system similar to Rust's macro_rules would be superb).

Is the benefit here that there isn’t another type or something else?

We don't need to open up arbitrary token macros—a macro that could accept a PatternSyntax would be sufficient to solve this.

It's possible today to get something pretty close to what Rust has, because a subset of interesting patterns are also valid expressions. For example, I whipped together a quick-and-dirty macro that expands this:

if #matches(subject, MyEnum.foo, MyEnum.bar(10)) { ... }

to this:

if ({
  switch subject {
  case MyEnum.foo, MyEnum.bar(10): true
  default: false
  }()) { ... }

But Rust's matchers also allow a guard equivalent to our where clauses, and you can't write something like this:

if #matches(subject, MyEnum.foo(let x), where: x % 2 == 0) { ... }

because MyEnum.foo(let x) isn't a valid expression.

2 Likes