Proposal draft for `is case` (pattern-match boolean expressions)

+1 and to reiterate others, prefer precedence matches is. Additionally a fix-it to apply parens for clarity would be nice

2 Likes

I feel this is one of the important reasons why is case would be better.
It is inconvenient that autocompletion doesnā€™t work with if case.

1 Like

I feel awkwardness with is case, as is is type-casting-operator.
In pseudo-codes here it is becoming value-comparison-operator (although only if followed by case).

enum Fruit: Equatable {
    case apple(quantity: Int)
}

let fruit: Fruit = .apple(quantity: 4)
         let _: Bool = fruit == .apple(quantity: 4) // OK
/* 1. */ let _: Bool = fruit == .apple(_) // error   '_' can only appear in a pattern or on the left side of an assignment
/* 2. */ let _: Bool = fruit == .apple    // error   member 'apple(quantity:)' expects argument of type 'Int'

If 1. or 2. would be compilable, what would be the concern and why rather introducing new syntax preferable? (Plus you will also get negation as a bonus in this case)

I mean, to introduce new syntax or keywords, there needs to be convincing reasons why reusing existing keywords is not enough, I guess.

Excuse me if I'm missing a point.

I also like @tera 's extending approach!

2 Likes

There are certain important parallels between types and enum cases. (Indeed, once upon a time, enum cases were capitalized.)

3 Likes

Capitalized, not camelCased.

enum E {
  case One, Two, Three
}

+1 from me. This solves a common frustration and fits well with existing syntax.

1 Like

Big +1 from me.
The only thing that would be better is synthesized Bool properties (bc key paths are so great) ā€” but the proposed syntax makes the most sense given existing keywords (and is less "magical"). Introducing this would also make hand-crafted Bool properties much simpler to write, so I'm here for it!

RE: Implementation
What does implementing a feature like this in the compiler involve these days? Are there any guides or documentation? This seems like it wouldn't be tough to implement, but ramping up always seemed daunting. Last time I looked into it was like 2018. Contributing something to Swift has always been a goal of mine, but I never got past the first hurdle.

1 Like

I think the functionality is definitely needed. On another line of evolution, we're starting to look into factoring enum cases and other conditionally-available storage into key paths (and there's an implementation in progress. One bit of convergent evolution that key path support for enum cases suggest is the ability to project out payloads as instance members of the enum: if you can write structValue[keyPath: \.property] in order to get the value of struct.property, then it seems reasonable to expect that enumValue[keyPath: \.case] should give you the same value as enumValue.case would. And if that projection gives you a Bool or Optional, then you can test it within an expression using existing idioms like enumValue.case != nil.

<expr> is case <pattern> is still strictly more general since it works with any pattern, but it also seems like enum payload testing is likely to be the most common use case for it today. How much do you think we'd yearn for it if we already had a mechanism for testing enum cases in an expression? And if we had both, would you all as developers prefer to write enumValue is case .x or enumValue.x != nil?

13 Likes

I donā€™t quite follow. Are you suggesting that enumValue.x would evaluate to nil if enumValue is a case other than x? That seems confusing, especially if xā€™s payload is an optional. Iā€™d rather have to decompose enumValue specifically in order to access its payload.

2 Likes

I've never been a fan of the != nil idiom, but it might read better if Optional had a hasValue property or something like that, so you could write enumValue.x.hasValue.

3 Likes

Iā€™m still unclear on whether you are proposing hasValue or != nil as an alternative for is case. enumValue.x.hasValue is vacuously true if enumValue is case x is false.

If we made enumValue.x return an Optional of the payload, with .some value if enumValue is of case x or .none if it's of a different case, then enumValue.x != nil, enumValue.x.hasValue, and enumValue is case .x would all be true when enumValue is of case x.

Right, but enumValue.x.hasValue == false defies the rules of classical logic when enumValue is some other case than x.

It is much clearer to me when itā€™s spelled out if case let payload = enumValue.x, somePredicate(payload).

1 Like

I actually like this. Doubly so if I can further access enumValue.x.a in a read only or read write manner (for case x(a: Int, Int)).

Changing individual payload components would be a game changer (effectively "mutable enum values", which we currently don't have).

Same would be with a dictionary having optional values, which we allow.

You'll still be able doing so as the current way is not going anywhere.

1 Like

Wouldnā€™t it be enumValue.x?.a?

I know. I'm just concerned that conflating cases with optional members might be easily misread.

enum AuthorizationState {
  case notAuthorized
  case authorized(username: String, birthday: Date?)
}

let authState = AuthorizationState.notAuthorized

if Date.now == authState.authorized?.birthday ?? Date.distantFuture {
  // Did you assume that the user is authorized?
  // Will someone who reads your code later make that assumption?
  // Maybe you typed `authState.authorized.birthday` and you quickly accepted the "?" fixit instead of thinking about your state.
} else {
  // This is a weird blockā€¦ we get here if weā€™re not authorized or if we are authorized and itā€™s not our birthday.
}

Enums are all about exclusivity between cases and the case-as-optional-member syntax blurs that significantly.

1 Like

Modeling cases as members also invites some weird lookup questions:

enum Devious {
  case someCase(flatMap: String)
  case otherCase
}

let d = Devious.otherCase()

print(type(of: d.someCase.flatMap)) // `String`, or `<U>((flatMap: String)) throws -> U`?

Edit: I guess per my previous post it would have to be the latter (or more accurately a compile error because Swift doesnā€™t have generic closures), because the former is spelled d.someCase?.flatMap. And it would actually be String?.

1 Like

I don't want to derail the is case design discussion too much; there are definitely design questions to iron out exploring this other direction.

armchair analysis of that example

In your example, if d.someCase gave an optional value, flatMap would always refer to the member on Optional. We notionally accepted SE-155, which was supposed to bring enum cases in line with other function declarations and make payload labels part of the decl name (although the implementation isā€¦incomplete); by that guideline, d.someCase should give an unlabeled tuple of its fields as the result. However, if we did carry the labels over into the result tuple type, then the Optional wrapping still means you have to go through one of Optional's members before you could get to the tuple's flatMap member.

Yes, I have only been asking questions about the other design because I think doing so illustrates that is case is a comparably simpler design and therefore worth pursuing.

5 Likes

As far as I can tell from Swift Twitter, adding optional accessors named for the corresponding cases and returning the associated value is already widely used in the field, but people resort to writing it manually, or use one of the available preprocessors to do it for them.

Sourcery Stencil
Point free
Hacking with swift also has a tutorial for the sourcery / stencil system.

This suggests that the pain of double optionals is either acceptable to get the benefits, or not a problem in most cases.

This is some great news. Some follow up questions regarding payload key-paths:

  • Will that topic be brought to the evolution forums?
  • It seems like the payload for an enum case without an associated type is Void. Why isn't it Never instead?

The second question is the reasoning for the first one. I would like to know why Void is the right payload type there instead of Never and if it's something that is already fundamentally anchored deep in the language or still debatable, hence the request for an evolution discussion / pitch + proposal.

1 Like