Pitch: 'Case Expressions' for Pattern-matching

Hi all,

I’ve noticed a few common patterns recently in code that makes use of ‘if case’ statements:

if case .someEnumCase(_, someMatchingValue) = x {
    return true
}
return false

if case .someEnumCase(_, someMatchingValue) = x {} else {
    // do something only in the negative case
}

if case .someEnumCase(_, someMatchingValue) = x {
    dict[someKey] = true
} else {
    dict[someKey] = false
}

All three examples seem unnecessarily clunky due to the current restrictions on the pattern matching syntax. I’d like to pitch a proposed syntax for ‘case expressions’ that would allow us to rewrite the three examples above as:

return case .someEnumCase(_, someMatchingValue) = x

if !(case .someEnumCase(_, someMatchingValue) = x) {
    //do something only in the negative case
}

dict[someKey] = case .someEnumCase(_, someMatchingValue) = x

While this would make it easier to compare enum cases without associated values, I think the feature would still be quite useful even if we adopt one of the solutions to that problem discussed here. I took a look at a ~100k LOC codebase and found a few dozen instances of the patterns above where either some associated values were being matched while others were ignored, or a custom ~= implementation was being used. To give a more realistic example which goes beyond a simple case test:

enum PaymentMethod {
    case cash
    case check(number: UInt)
    case creditCard(number: String, cvv: String, expirationDate: Date)

    var isVisa: Bool {
        //Assume there’s an appropriate ~= implementation for Regex and String
        return case .creditCard(number: Regex(“^4[0-9]{12}(?:[0-9]{3})?$”), cvv: _, expirationDate: _) = self
    }

    var isLowNumberedCheck: Bool {
        return case .check(number: 0…999) = self
    }
}

The syntax is intended to be easily discoverable for users who are already used to pattern matching elsewhere in the language, though it is a little more heavyweight than just introducing a new binary operator, and would add some complexity to the grammar. I’d appreciate any and all feedback/suggestions as to whether people think this might be a worthwhile addition before fleshing out this pitch some more. Thanks!

7 Likes

This makes users think that 'case ...' returns a value. Besides than this slightly unclear sugar, though, this proposal does have some ground. I think recently there was a thread on Using Swift similar to this. I find it as it may be relevant.

1 Like

I found it:

This thread supports a not keyword that can be added to case. Thinking about it, Swift chose not to use keywords for Boolean operators and rather the symbols which makes me unsure if this is consistent with the current Swift syntax.

1 Like

Sorry, I think truncating the condition may have made my intent a little unclear, I've edited my original post to clarify. In what I am currently proposing, given

if !(case .someEnumCase(_, someMatchingValue) = x) {
    //do something only in the negative case
}
(case .someEnumCase(_, someMatchingValue) = x)

Would be a boolean expression (as opposed to a statement condition), negated by the existing boolean ! operator.

I do like the idea, but not the proposed syntax. What about

if case .someEnumCase(_, someMatchingValue) != x {
}

This kind of overload the != operator, which is not very satisfying, but feels less convoluted that !(case = ) to me.

3 Likes

I agree that your syntax reads better when used in a condition, and might be a good future direction for a feature like this (I tend to agree that != is a less than ideal spelling though). However, I think it would still be valuable to have a more general way of writing pattern matching expressions which evaluate to a Bool on success/failure to handle other cases besides negation, like the first and third examples from my original post where true/false are being returned or assigned right away.

case .someEnumCase(_, someMatchingValue) = x is in fact an assignment (to someMatchingValue) . It also does return a boolean value to the if-statement, similar to if var v = x {.

However, it is not an expression whose value can be assigned to a variable. In that sense if !(case .someEnumCase(_, someMatchingValue) = x) { would extend this syntax of an non-expression with a boolean value to the negative case.

I also think that if case .someEnumCase(_, someMatchingValue) != x { looks more readable, but in this form someMatchingValue is a parameter value and no longer a variable to be assigned to.

I have been in favor of not as a negation operator instead of ! ever since I switched from Pascal-like languages to C and its derivatives, because while I love conciseness, I prefer readability. Swift worsens the issue by overloading the almost invisible operator ! to even more uses. But that's a bigger question.

It only "assigns" to someMatchingValue (really, "binds"), if you write case .someEnumCase(_, let someMatchingValue). Otherwise it's part of the pattern:

enum SomeEnum { case oneCase; case anotherCase(String, Int) }
let x: SomeEnum = ...

if case .anotherCase(_, 42) = x { /* ... */ }

if case is definitely a weird construct, and you've mentioned some of the really awkward limitations it has. I think it would be worth making a general effort to make most of our language constructs usable as expressions. For example, the return and assignment examples could be handled by making if an expression.

The topic has come up several times before and it appears that some members of the core team support the idea. You might want to also check out these threads:

1 Like

Thanks for bringing up those past discussions, it’s good to see there have been some past attempts to tackle this and similar problems! I wonder if a better initial step towards parity between control flow statements and expressions might be to replace the Boolean ternary argument with a statement condition (which could be an expression, case match, or availability condition). It wouldn’t be as concise as what I proposed above, but might be less surprising and allow pattern bindings in the true branch as well.