[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