[swift-evolution] [Pitch] Reimagining guard case/if case
Brent Royal-Gordon
brent at architechies.com
Wed Oct 26 05:12:07 CDT 2016
> This proposal replaces the current syntax with a simpler grammar that prioritizes pattern matching but mirrors basic conditional binding. The new syntax drops the case keyword and replaces = with ~=. The results look like this:
>
> guard let .success(value) ~= result { ... }
> guard .success(let value) ~= result { ... }
> if let .success(value) ~= result { ... }
> if .success(let value) ~= result { ... }
> guard let x? ~= anOptional { ... }
> if let x? ~= anOptional { ... }
I don't mind this syntax, but I'm not convinced this is worth doing if it's merely a syntax change. (It goes without saying that this is not phase 1.) But if it were motivated by a broader change, I think I could get behind it.
Suppose we introduced this concept:
protocol Pattern {
associatedtype Target
associatedtype Bindings
static func ~= (pattern: Self, target: Target) -> Bindings?
}
// Sometimes you just want the pattern on the other side.
func =~ <PatternType: Pattern>(target: PatternType.Target, pattern: PatternType) -> PatternType.Bindings? {
return pattern ~= target
}
And further suppose that conditionals were altered to support `~=` and `=~` instead of `case =`. That is, a conditional with a pattern match in it would take the "then" branch if the `~=` returned `.some`, and would bind the variables in the returned tuple. In essence, this:
if pattern ~= target { … }
Would be rewritten to:
if let ([variables from pattern]) = (pattern ~= target) { … }
(One way to do this would be to support *any* Optional as a conditional, not just the results of a pattern match; this would essentially make `nil` into a false-ish value. But we tried that in the Swift 1 betas and didn't seem too happy with it.)
Enum cases would then have a dual nature; they could construct values, but they could also construct `Case` instances:
let value = Foo?.some(Foo())
// value: Foo?
let pattern1 = Foo?.some(let value) as Case
// pattern1: Case<Foo?, (value: Foo)>
let pattern2 = Foo?.some(_) as Case
// pattern2: Case<Foo?, (Foo)>
let pattern3 = Foo?.some as Case
// pattern3: Case<Foo?, (Foo)>
let aFoo = Foo()
let pattern4 = Foo?.some(aFoo) as Case
// pattern4: Case<Foo?, (Foo)>
Note that all four patterns are some variant of `Case<Foo?, (Foo)>`; it's just a matter of re-labeling the second parameter. Hopefully you could do that with a cast:
if (pattern as Case<Foo?, (foo: Foo)>) ~= optionalFoo {
// use `foo` here
}
// Or with more powerful existentials:
if (pattern as Pattern where Bindings == (foo: Foo)) ~= fooTarget {
// use `foo` here
}
// Perhaps some sugar:
if fooTarget =~ pattern as let value {
// use `foo` here
}
Elements with a label are bound to a variable by that name; elements with no label are not bound to any variable.
`Case` would look something like this, with the actual implementation of `~=` being compiler magic:
class Case<Enum, Associated>: Pattern {
typealias Target = Enum
typealias Bindings = Associated
static func ~= (pattern: Case, target: Target) -> Bindings? { … }
}
(I suppose it might instead be `Enum.Case<Associated>`—just depends on whether we want it to be magic or standard library-ish.)
So, what other patterns could we implement? Well, most notably, regular expressions:
class Regex<Captures>: Pattern {
typealias Target = String
typealias Bindings = #concatenateTuples(($0: String), Captures)
static func ~= (pattern: RegularExpression, target: String) -> Bindings? { … }
}
You would use them like this:
if /ab(.(.)?)c/ ~= myString {
// That's a Regex<($1: String, $2: String?)>.
// A named capture syntax would allow you to rename $1 and up, or you could cast
// a regex you had received.
// If string slicing is corrected so that substrings share indices with their parent string,
// you would not need a separate "get range of match" feature.
}
But here's another neat use case:
let gregorian = Calendar(identifier: .gregorian)
if myDate =~ DateComponents(calendar: gregorian, month: 10, day: 31) {
print("👻🎃🕷⚰🍫")
}
extension DateComponents {
typealias Target = Date
typealias Bindings = (DateComponents) // Optionally bindable
static func ~= (pattern: DateComponents, target: Target) -> Bindings? {
let calendar = self.calendar ?? Calendar.current
let timeZone = pattern.timeZone ?? TimeZone.current
guard calendar.date(target, matchesComponents: pattern) else {
return nil
}
return calendar.dateComponents(in: timeZone, from: date)
}
}
Anyway, just a related thought I had. Sorry for hijacking.
--
Brent Royal-Gordon
Architechies
More information about the swift-evolution
mailing list