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

I like this. It is quite similar to this one above (updated) with some differences, e.g. my version introduces dynamic variables with names matching the names of enum constants and does not generate an explicit nominal type (or a type alias), assuming the type would match the (tuple) type of the associated values for the case (and for a case value without associated value the type matches the type of the enumeration), the type is deliberately implicitly unwrapped optional which I believe is a good thing in this case, accessing associated values either by name or by index, and new to swift - write access for individual associated value variables. This alternative doesn't have all features of the pitch proposal and vice versa, the pitch proposal doesn't have all features of this alternative - still there's a significant overlap between the pitch and the alternative (feature wise), and there's a huge overlap between the two alternatives being discussed.

:new: updated the sample in the linked post.

1 Like

As much as I'd like the consistency that we'd get from allowing this last line to compile, to match the others…

if case 0... = 1 { }
if 0... ~= 1 { }

let optional = Int?.none

if case .none = optional { }
if .none ~= optional { }

if case .some = optional { }
if .some ~= optional { }

…I think that ship has sailed. If you were to solve this with ~=, enum cases with associated values would only sometimes be closures when not "prefixed" with case, whereas now, they predictably always are closures.


My current solution, that I would love to delete in favor of is case, uses that feature and looks like this:

if Optional.some ~= Never?.none { }
if .none ~= Void?.none { }
/// Match `enum` cases with associated values, while disregarding the values themselves.
/// - Parameter case: Looks like `Enum.case`.
public func ~= <Enum, AssociatedValue>(
  case: (AssociatedValue) -> Enum,
  instance: Enum
) -> Bool {
/// Match non-`Equatable` `enum` cases without associated values.
public func ~= <Enum>(pattern: Enum, instance: Enum) -> Bool {
2 Likes

+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.

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!

1 Like

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

2 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?

12 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?.

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.

3 Likes