Proposal sanity check: assigning a case statement to a boolean

Given that case can appear in an if statement without variable assignment, it has me scratch my head a bit why it can't be used to assign a boolean value.

In other words, seeing as this code works,

enum Enum {
  case aCase
  case anotherCase
}

let value = Enum.aCase

if case .aCase = value {
  print("a case")
} else {
  print("another case")
}

then it follows that this should as well,

let bool = (case .acase = value)

The only stumbling point seems to be that it uses a single equals sign instead of a double equals sign, which doesn't make much sense in the first place. In the if case if there's no variable assignment the syntax should also use a double equals to be clear, perhaps even if there is variable assignment.

It could be that the original proposal did not consider all of the ramifications it would have. This could leave room open for new proposal to tackle these cases, as it were.

7 Likes

I think most of case patterns don't make sense to be boolean. The only real case would be on complex enum without value bindings. Even then, it does overlap a lot with normal == operator.

Why would you want to write it that way rather than let bool = value == .aCase?

3 Likes

It's not a "case expression" in an if statement, it's an if case statement, in the same way that if let is not an if statement with a "let expression".

Also, it really is an assignment, although you have to look at a more general form:

if case .aCase(let associatedValue) = enumValue

There's a generalized/conceptual source and destination here, and a value is being copied from the source to the destination, which is what an assignment does.

I agree it's a bit weird when there's no associated value. It's just one of those Swift quirks that you have to memorize.

4 Likes

This won't work either when the enum has a payload, which I presume is what we are trying to solve here. I agree with Ilias that it is often extremely useful to "make any enum case equatable" so to speak.

I think others have pitched ways to compare just the bare cases of enum case with payloads, and this seems like the most natural way.

1 Like

Being able to assign the result of pattern matching to a boolean would be great. The main issue is that the pattern wouldn't be able to bind any variables, since there would be no scope for those variables to live in, but being able to get the result of the test is useful.

16 Likes

There's certainly a nice little feature that can potentially be implemented here, which would allow ergonomic checks even when the value is not Equatable (e.g. a payload is not Equatable) or you want to check for specific literal payloads. I've filed [SR-13601] Boolean-valued case checking expressions · Issue #56036 · apple/swift · GitHub for a slightly more general version of what you've brought up in the example.

4 Likes

More syntax bikeshedding: case color is .rgb(1.0, _, _).

Upside: not confusable with an operator or an assignment.
Downside: order is reversed from today's if case.
Upside: today's if case is also considered weird, and it's bad for code completion when you type left-to-right, and this would fix that too.
Downside: has nothing to do with the other is, which is even part of pattern syntax already (case is UIView:).

let flag = case enumValue is .aCase
if case enumValue is .aCase(let associatedValue) { … }
14 Likes

How about color is case .rgb, since case usually appears with the specified enum case?

14 Likes

I would personally prefer is case as a standalone token as much as for case or if case do.
It's also easier to read and cannot be mistaken for is (is accepts a type, is case would only accept a case, which is either an Enum value or an Enum constructor function, which are values and not types):

let flag = enumValue is case .aCase
if enumValue is case .aCase { ... }

Edit: @GreatApe beated me with the timing :joy:

9 Likes

That absolutely reads better as an expression :-) I'm a little worried about it doing funny things in if though:

if case .aCase(let assoc) = enumValue { // today
if enumValue is case .aCase(let assoc) { // okay
if (enumValue is case .aCase(let assoc)) == false { // error!
if enumValue is case .aCase(let assoc) == false { // um

I mean, we can make that work (basically by delaying the "can you put bindings here" check to after the "what kind of if is this" check), but it feels subtle to me in a way that the connected initial if case doesn't. Maybe that's just me, though.

For what it's worth, is case would not only accept a case; it would accept any pattern, the same as switches and ifs:

let isOnXAxis = point is case (_, 0)

I don't think this is a problem, though!

4 Likes

But would you need to be able to bind when using this new construct? Can't you always revert to if case when there is binding going on? To me the second example is a bit confusing, and at least this time it is unnecessary, right?

And why is the third one not ok?

1 Like

I would not use is case to extract associated values since the let/var keywords shouldn't be on the right side in an assignment expression. I would use something like as? case or as! case to extract payloads as written here and let enumerations gain the same benefits optionals have.

let associatedValue = enumValue as! case .aCase // force unwrapping
if let associatedValue = enumValue as? case .aCase { ... } // unwrapping

But this is far beyond the purpose of this thread.

2 Likes

Oh huh, I didn't even think of extending this to the as? / as! spectrum. I'll avoid nitpicking your examples because you're right that it's out of scope. My main thoughts about having this new syntax work with if were mostly about having related constructs look alike; it's too bad that switch and is case would look pretty similar and if case would be the odd one out. But you're right, we don't need to support that for this other part to be a good idea.

if (enumValue is case .aCase(let assoc)) == false { // error!

In this example, assoc is only valid to use if the case matches, but "if the case matches" doesn't say anything about "the body of the if will run" if it's composed with arbitrary expressions, so the compiler can't assume in general that assoc is safe to use. That's (part of) what Joe meant by "there would be no scope for those variables to live in".

I've always considered adding expression syntax for pattern-matching to be bound up with the idea of supporting something like regular expressions. And I wouldn't want the syntax to only work with a Bool result; you ought to be able to extract matched values in some way. So it's actually a rather large feature that needs to be thought through, not something we should add just because we can easily imagine its simplest form.

12 Likes

Ah ok. But how about a solution where is case is used merely to make enum cases with associated payloads "equatable" without supplying or binding a case? In other words, it would only be used as an expression:

enum G {
    case one(Int)
    case two(Int, Int)
}
...
let isOne = someValue is case .one

This is the usage I personally miss the most. My intuition says that if we ever want to bind the associated value, then we can instead use if case as today, but I may be wrong...

3 Likes

Excuse my lack of imagination, but what else could be the result of the pattern matching, except a Bool?

I've thought about this too, and the conclusion I've come to is that something that produces bindings is always going to be attached to a scope. For anything that doesn't produce bindings, a method call or operator is fine. But we don't have that for structural patterns because they're part of the language rather than the library—we don't have a representation of a "pattern" that can be passed around at runtime.

The other thought I've had is that something like regex captures, which do both matching and binding, are extremely difficult to make syntax for that's both (reasonably) easy to read and statically type-safe. I absolutely want us to solve that problem but I don't want to block other things on it.

(This idea has long been blocked on the idea that we'd generate "implicit properties" on enums for checking and destructuring a particular case, but even that doesn't cover other sorts of pattern matching, so maybe we shouldn't let that block this idea either.)

5 Likes

Well, if it produces a binding, yes, the bound name has to have a scope. But it could also produce a result, which we could tuple together appropriately and wrap in an optional, and then the user can do whatever they like with that value. The typing rule would be very much tied to the form of the pattern, but I think that's what you'd expect.

Ideally the "regexp" syntax would have the same dual-use properties: if you used it in a case, you could bind names directly to certain matches, and otherwise you would get the matches back tupled up and wrapped in an optional.

4 Likes

Personally I quite dislike that backwards-reading case syntax:

if case .aCase = value {
    print("a case")
}

I'd love to be able to write the more natural:

if value == .aCase {
    print("a case")
}

Then the assignment looks very familiar as well:

let bool = (value == .aCase)

In all of the above, I don't care whether the enum has associated values or not, and the syntax is exactly the same either way.

To retrieve an associated value, I wish I could write something similar to this:

if value == .aCase(let associatedValue) {
    print("a case with \(associatedValue)")
}