I don’t agree with the strategy of using this pitch as a Trojan horse to reinvent Swift’s pattern matching syntax. That seems like a completely separate discussion.
The main issues I see with matches is that it introduces a new keyword instead of reusing is and case, and it also makes the path forward to expressive case extraction even more ambiguous, as is case at least suggests as case.
destination is case .messageThread // true
destination as case .messageThread // Optional(42)
What would the equivalent be for matches?
Drop case?
destination is .messageThread // true
destination as .messageThread // Optional(42)
This is great. It would be really nice to avoid the switch dance when reducing to boolean. (I really appreciate the acknowledgments.)
wrt bikeshedding names, among the "Alternatives considered" is the spelling without is:
// <expr> case <pattern>
// Not evocative of a boolean condition
HeaderView(inThread: destination case .messageThread)
I prefer this as being consistent with case patterns. Without is, the difference between case patterns and case expressions is that in a pure expression context, variables are not bound.
I don't like "is" because it means virtually anything, encourages the notion that code is to be read aloud to be understood, and requires case anyway to make any sense.
I don't like "matches" because there are many kinds of matches, and it only indirectly suggests swift patterns. (Similarly, =~ is too similar to ...)
By contrast, case alone is already a keyword, and clearly restricts the domain to enumerations.
One expression usage is the ternary expression used to simplify a complex case without resorting to the full switch:
enum Domain {
case unstated, undermin, min, negative, zero, positive, max, overmax
func defined() -> Bool? {
(self case .undermin || self case .overmax || self case .unstated)
? nil
: !(self case .zero)
}
}
I think is would make this less clear.
This raises a separate issue, whether case expressions in an enumeration context can elide self:
enum Domain {
...
func defined() -> Bool? {
(case .undermin || case.overmax || case .unstated)
? nil
: !(case .zero)
}
}
Is case seems weird and clunky, but both this suggestion and matches suffer from the problem that they are leas powerful than an actual discriminant enum for example; you can do does NOT equal, nor making an array of discriminants for example. For these reasons I definitely think we should think more about this and come up with a more powerful solution.
For example, you could let case operate on the other side, and turn the unknown instance into its discriminant:
let isOpen = case currentState == .open
// where currentState is
enum State {
case open(String)
case closed(Int)
}
// and case currentState is of this synthesized type
enum State.Case {
case open
case closed
}
I object to the characterization of a “Trojan horse”. That phrase implies deception and malicious intent.
I am being clear and direct in two goals that I see as desirable:
1. I want to improve this pitch by changing the keyword from is case to matches.
2. I want to improve Swift’s existing pattern-matching by changing if case to if ... matches.
Furthermore, item (2) is not a completely separate discussion from this pitch.
Notably, if this pitch were to be accepted as-is, then that would effectively lock in the spelling is case. Perhaps there might be support for a follow-on proposal to enable variable binding in if ... is case and thereby supersede if case, but it would be essentially impossible to change the keyword from “is case” to “matches” after the fact.
Thus, the proper spelling of the keyword needs to be decided before any proposal in this space gets accepted.
my feeling is this was already effectively “locked in” many years ago. to rename this keyword and rearrange its syntax would be massively source-breaking, to an extent that the cost of migrating to the new syntax would not be worth the (marginal?) improvement in clarity it would yield.
As I said above, I understand that we cannot actually remove if case from the language. But we can introduce a better spelling for it, so no one will want to use if case anymore.
The spelling is case does not yet exist in the language. It is not locked in. We can choose whether to use that spelling or a different one like matches.
The spelling
is casedoes not yet exist in the language. It is not locked in. We can choose whether to use that spelling or a different one likematches.
I do think it is better, for this pitch, to evaluate the utility of an inline pattern matching syntax rather than a divergent keyword change to the language.
I don't think that the addition of is case would preclude eventually changing the pattern matching syntax to use matches. In fact, should we end up wanting to change the pattern matching syntax, we could do a holistic change and change it wherever a pattern may be used (and even consider the switch statement like below):
// inline checks
destination is case .messageThread
destination matches .messageThread
// if condition
if case .messageThread = destination {
if destination matches .messageThread {
// within switch
switch destination {
case .messageThread: ...
matches .messageThread: ...
}
case let statements have always been very verbose and cumbersome to work with. Especially when your associated values are also enums
I am strongly in favor of this.
I share @Nevin's concern about the is case spelling, and the fact that it conflates 2 meanings:
- matching a pattern with the
casekeyword (as is the case today, pun intended); - asking if an enum is in a particular
case, regardless of the associated values.
I also am not opposed to the idea of introducing a new keyword for solving an existing issue if we're going to introduce completely new syntax.
It might be productive to take a step back. One of the original design criteria of Swift was the familiarity with other C-like languages, while using progressive disclosure to "enhance" the meaning of existing declarations with more powerful features.
So, for example, an enum is defined by using the case keyword for each case, but the latter can also have associated values, turning it into a sum type. Also, the switch statement uses the case keyword for identifying the cases of an enum, but each case ...: declaration is effectively a pattern match.
We could imagine for example an alternate universe where Swift switch statement was spelled like this:
let value: Result<Int, Error> = .success(42)
match value {
as .success(let wrapped): print(wrapped)
as .failure: print("failure")
}
In addressing the need to simplify and improve code like
let value: Result<Int, Error> = .success(42)
let isSuccess = if case .success = value {
true
} else {
false
}
we should bear in mind that original criterion (that I think it still valid today).
Assuming that we want to introduce new syntax, I'd personally consider the following constraints:
- it should avoid the issue of a declaration like
if case .some = value, which reads backwards, and it's complicated to understand and use for beginners; - it should allow for something like
case .some = valueto be used as an expression in itself, that returns aBoolvalue; - it should use the
casekeyword in the exact same way it's used today, that is, matching a pattern; - it should be extendable in the future to allow binding variables, that are then returned by the expression (for example,
let wrapped: Int? = case .success(yield) = value.
Given the constraints, I would introduce the match keyword, that works like switch but for matching a single case:
let value: Result<Int, Error> = .success(42)
let isSuccess = match value case .success
This way, case has exactly the same meaning, and we can avoid the is case spelling that makes it look like we're asking if "some expression is in a certain case" rather than "some expression matches a certain pattern".
The examples in the pitch would then read:
match foo case .bar // enum case
match foo case .bar(42) // enum case with associated values
match foo case .bar(42, _) // enum case with partially matched associated values
match foo case 42 // integer literal
match foo case true // boolean literal
match foo case "A string" // string literal
match foo case bar // other expression
This would extend nicely to if statements:
let value: Result<Int, Error> = .success(42)
if match value case .success {
print("is success")
} else {
print("is failure")
}
Exploring "drop case" idea further:
enum E {
case foo
case bar(Int)
case baz(Int, Int, Int)
}
var value: E = ...
value is .foo // true or false
value as? .foo // ()? or nil
value as! .foo // () or crash
value is .bar // true or false
value as? .bar // Optional(42) or nil
value as! .bar // 42 or crash
value is .baz // true or false
value as? .baz // (1, 2, 3)? or nil
value as! .baz // (1, 2, 3) or crash
I like how is case reads, could be a great addition to the language.
I would vote against allowing variable binding, to avoid developers having to decide between using if case let vs if is case.
Without variable binding, however, I cannot but think this expression is an equality expression, and wonder why not use == instead, like this:
foo == .bar // enum case
foo == .bar(42) // enum case with associated values
foo == .bar(42, _) // enum case with partially matched associated values
foo == 42 // integer literal
foo == true // boolean literal
foo == "A string" // string literal
foo == bar // other expression
There is only a mention about using == in the pitch but I’m not sure where the ambiguity is.
How would these two following statements be different to the compiler?
HeaderView(inThread: destination == .messageThread(_))
HeaderView(inThread: destination is case .messageThread(_))
If == was deemed not appropriate, maybe the existing match operator ~= would be a better option, making it more usable and common for developers.
Whichever ends up being, I like this proposal.
These are actually the same meaning; specifically, the latter is a subset of the former.
I wonder if the reason people are getting hung up here is because they associate case with enum cases more strongly, due to history in other languages. But if we focus on Swift holistically, the case in if case is really analogous to the case in a switch statement, not to the cases in an enum. The latter are just value constructors, and the relationship to the other case is that they happen to let you involve their discriminator in that matching operation—that's why the syntax to match them is .name, not just name as they're declared in the enum. So it's that usage of the keyword that's the odd one out, not the one used for matching.
In my opinion, the awkwardness around if case historically in the language isn't around the if case <pattern> part, because that's written exactly as you would write it in a switch statement, but rather it's around the reversal of the ordering between the pattern and the value being matched. Since you have to write if case <pattern> = <value>, it reads in the opposite way that you'd expect, and it also causes problems for tooling like auto-completion because, if you type the pattern first, the IDE doesn't have the context to infer what the valid patterns are. is case doesn't suffer from this problem.
Those issues aren't the fault of the case keyword specifically. I wonder if a shift in thinking toward "case as in switch, not case as in enum" would help resolve many of the concerns here. When I think of it in those terms, is case fits perfectly with the existing usage in the language and I don't think a new keyword that would make a second way to do the same thing is warranted.
== is a function that takes two values of the same type and compares them. But what we want here, is to match a value to a pattern, which is not required to be a complete value. E.g. in your example foo == .bar(42, _), we have the pattern .bar(42, _) which we cannot pass to == in any way since it's impossible to represent it as a value of the same type as foo is. So the big difference between == and the proposed is case is that the former matches two values against each other while the latter matches a value against a pattern which is a fundamentally different thing.
On top of that == requires Equatable, so even when it "works" (value == .foo) it's not what we want, as the type is not necessarily Equatable.
I believe everyone here is in agreement that variable should be on the left and discriminator on the right, essentially we are looking for XXX and/or YYY:
value XXX .baz // false or true
potentially also:
value XXX .baz(1, 2, 3) // false or true, do we need this?
value XXX .baz(1, 2, _) // false or true, do we need this?
value YYY .baz // (1, 2, 3) or nil, do we need this?
and discussing what XXX should be, and also whether we want all flavours of XXX or not/or YYY as well.
The variants discussed so far for XXX:
is caseiscasematches- ~=
- nothing (use YYY instead, e.g.
value.baz != nil)
and for YYY:
-
as? case,as! case -
as?,as! -
<nothing>(e.gvalue.baz).
Everyone is probably in agreement, except me. I still think it would be nicer to have an operator that converts the unknown enum value to its discriminant, then that result can be used with ==, !=, put in an array or whatever. Seems more flexible and modular.
AFAICS, your approach wouldn't allow doing something like foo is case .bar(45, _). So how is it more flexible?
It would be more flexible in the ways I mentioned above. But you are right that it probably wouldn't support partial matches.
I am sort-of in favor of this proposal. But personally I wonder if this shouldn't be pushed a bit further.
The way I see it, a pattern without bindings is pretty much equivalent to a function (or closure) that takes some input and returns a boolean: a predicate. So we just need to allow case .foo(1.0, _) to convert to a type and then provide operators to use that type.
For instance, if we had this:
enum Foo { case a(Int, Int), b(Float, Float) }
let foo = Foo.a
let isMatching = foo ~= case .a(1, _)
This last line could be desugared like this:
let isMatching = foo ~= Pattern {
switch $0 {
case .a(1, _): return true
default: return false
}
}
Here, a Pattern type encapsulates a pattern matching closure, allowing the ~= operator to work and return a boolean telling you if there's a match. And since it's a type you can pass the pattern around pretty easily:
let pattern = case Foo.a(1, _)
let isMatching = foo ~= pattern
And so you could pass it to functions, allowing things like this (with the appropriate filter overload):
let foos = [Foo.a(1, 2), Foo.b(3, 4), Foo.a(5, 6)]
let matchingFoos = foos.filter(case .a(1, _))
This direction seems much more interesting that the current proposal... except perhaps for the part where introducing a type would make back-deployment difficult.