[swift-evolution] guard let x = x

Erica Sadun erica at ericasadun.com
Tue Nov 1 13:42:38 CDT 2016


With massive revisions. I've added Xiaodi Wu to author list. I've combined all unwrapping together. I've added a much more sensible approach (plus a bluesky one) for real world enumerations.

Gist with live updates here: https://gist.github.com/erica/db9ce92b3d23cb20799460f603c0ae7c <https://gist.github.com/erica/db9ce92b3d23cb20799460f603c0ae7c>

Send feedback, I will continue to revise.

-- E


Better Unwrapping

Proposal: TBD
Author: Erica Sadun <https://github.com/erica>, Chris Lattner <https://github.com/lattner>, Xiaodi Wu <https://github.com/xwu> David Goodine
Status: TBD
Review manager: TBD
 <https://gist.github.com/erica/db9ce92b3d23cb20799460f603c0ae7c#introduction>Introduction

This proposal redesigns common unwrapping tasks:

It introduces the unwrap keyword for optional values
It re-architects guard case and if case grammar to support unwrapping more complex enumerations by dropping the case keyword and replacing = with ~=.
It applies unwrap to non-optional values
Swift Evolution threads:

guard let x = x <https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20161024/028440.html>
Using a bind keyword <https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20160201/thread.html>.
Fixing pattern matching grammar <https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20161024/tbd.html>
 <https://gist.github.com/erica/db9ce92b3d23cb20799460f603c0ae7c#motivation>Motivation

Unwrapping values is one of the most common Swift tasks and it is unnecessarily complex. This proposal simplifies this process and enhances code safety and readability.

 <https://gist.github.com/erica/db9ce92b3d23cb20799460f603c0ae7c#optionals>Optionals

Swift lacks a safe way to bind an optional to a shadowed same-name variable in condition clauses like those used in guard and if statements.

Compare:

guard let foo = foo else { ... } // redundant
guard case let .some(foo) = foo else { ... } // overly complex

guard unwrap foo else { ... } // simple
Using "foo = foo" fails DRY principles <https://en.wikipedia.org/wiki/Don%27t_repeat_yourself>. 
Using case let .some(foo) = foo or case .some(let foo) = foo fails KISS principles <https://en.wikipedia.org/wiki/KISS_principle>
unwrap guarantees that an unwrapped shadow uses the same name as the wrapped version. This ensures that a conditionally-bound item cannot accidentally shadow another symbol. It eliminates repetition and retains clarity. Further, the unwrap keyword is common, simple to understand, and easy to search for if Swift users are unfamiliar with it.

In the rare case that the binding is variable, use the re-imagined var ~= syntax

 <https://gist.github.com/erica/db9ce92b3d23cb20799460f603c0ae7c#general-enumerations>General Enumerations

Swift's guard case and if case statements stand out for their unintuitive approach. They look like assignment statements but they are not assignment statements. This violates the principle of least astonishment <https://en.wikipedia.org/wiki/Principle_of_least_astonishment>. This presents difficulties for new language adopters by combining several concepts in a confusing form. They are arguably underutilized by language experts.

The status quo for the = operator is iteratively built up in this fashion:

= performs assignment
let x = performs binding
if let x = performs conditional binding on optionals
if case .foo(let x) = and if case let .foo(x) = performs conditional binding on enumerations and applies pattern matching
Both guard case and if case statements perform simultaneous pattern matching and conditional binding. Here are examples demonstrating their use in current Swift:

enum Result<T> { case success(T), error(Error) }

// valid Swift
guard case let .success(value) = result
    else { ... }

// valid Swift
guard case .success(let value) = result
    else { ... }
The problems with guard case and if case include:

The = operator looks like assignment and not like pattern matching (~=). 
The case layout is both too close to a switch's case but doesn't follow its syntax. In switch, a case is followed by a colon, not an equal sign.
Using the case syntax is unneccessarily wordy. It incorporates case, =, and optionally let/var assignments.
 <https://gist.github.com/erica/db9ce92b3d23cb20799460f603c0ae7c#indirect-and-direct-pattern-matching>Indirect and Direct Pattern Matching

Swift uses two kinds of pattern matching.

Indirect pattern matching such as the kind you see in switch and for statements receives an argument in from the statement structure. The argument is not mentioned directly in the case:

switch value {
case .foo(let x): ... use x ...
...
}

for case .foo(let x) in collection { ... }
Direct pattern matching including guard/if statements and with the pattern matching operator place the argument to be matched to the right of an operator, either = or ~=. The argument is explicitly mentioned:

if case .foo(let x) = value { ... use x ... }
if 100...200 ~= value { ... }
When using if case/guard case in the absence of conditional binding, statements duplicate basic pattern matching with less obvious semantics. These following two statements are functionally identical. The second uses an assignment operator and the case keyword.

if range ~= myValue { ... } // simpler
if case range = myValue { ... } // confusing
 <https://gist.github.com/erica/db9ce92b3d23cb20799460f603c0ae7c#detailed-design>Detailed Design

This proposal introduces the unwrap keyword. The unwrap statement shadows an enumeration variable to an unwrapped version of the same type. Upon adopting this proposal the following statements produce equivalent behvior:

// New unwrap keyword
if unwrap myValue { ... }

// Existing same-name shadowing
if let myValue = myValue { ... }

// Existing same-name pattern matching and conditional binding
if case .some(let myValue) = myValue { ... } // old grammar
if case let .some(myValue) = myValue { ... } // old grammar

// Proposed same-name pattern matching and conditional binding
if .some(let myValue) ~= myValue { ... } // new grammar
if let .some(myValue) ~= myValue { ... } // new grammar
In if case and guard case, this proposal drops the case keyword and replaces the equal sign with the pattern matching operator. The results look like this:

guard let .success(value) ~= result else { ... }
guard .success(let value) ~= result else { ... }
if let .success(value) ~= result { ... }
if .success(let value) ~= result { ... }
guard let x? ~= anOptional else { ... }
if let x? ~= anOptional { ... }
Users may choose to use var instead of let to bind to a variable instead of a constant. 

In this update:

The case keyword is subsumed into the (existing) pattern matching operator
The statements adopt the existing if-let and guard-let syntax, including Optional syntactic sugar.
if let x = anOptional { ... } // current
if case let x? = anOptional { ... } // current, would be removed

if let x? ~= anOptional { ... } // proposed replacement for `if case`
On adopting this syntax, the two identical range tests naturally unify to this single version:

if range ~= myValue { ... } // before
if case range = myValue { ... } // before

if range ~= myValue { ... } // after
Using pattern matching without conditional binding naturally simplifies to a standalone Boolean condition clause.

 <https://gist.github.com/erica/db9ce92b3d23cb20799460f603c0ae7c#unwrap-and-non-optionals>unwrap and Non-Optionals

Real world Swift enumerations rarely follow the Optional pattern, which can be summed up like this:

enum OptionalLike<T> { case aCaseWithOneAssociatedValue(T), anotherCase }
They more typically look like this:

// One generic, one known type
enum Result<Value> { case success(Value), failure(Error) }

// Many cases of mixed types
enum JSON {
    case string(String)
    case number(Double)
    case boolean(Bool)
    case array([JSON])
    case dictionary([String: JSON])
    case null
}

// Multiple associated values
enum Response { case contact(code: Int, message: String), failure }
You can adapt the bind keyword to work with these real-world cases in one of two ways. The first way uses unwrapinstead of let or var. Here are a few varieties of how that call might look versus the proposed update for normal pattern matching:

if unwrap .string(myString) ~= json { ... }
if unwrap .contact(code, _) ~= response { ... }
if unwrap .contact(code, var message) ~= response { ... }

// vs proposed

if let .string(myString) ~= json { ... }
if var .string(myString) ~= json { ... }
if .contact(let code, _) ~= response { ... }
if .contact(let code, var message) ~= response { ... }
Although slightly wordier than let and var, the unwrap solution offers advantages:

It enhances readability. If the goal is to unwrap an embedded value, unwrap uses a more appropriate term.
It establishes one place for the keyword to live instead of the "does it go inside or outside" status quo. A consistent place means prettier code.
As shown in these examples, it can easily be adapted for variable binding. If you want to override the let behavior, you can insert a var inside the parentheses.
A second, riskier, cleaner approach looks like this. It assumes the compiler can pick up and shadow using either the associated value labels (preferred) or the enumeration name (as you'd see with String raw value enumerations):

if unwrap .contact ~= response { 
   ... some compiler magic picks up on the `code` and `message` labels
   used for initialization, so the bound variables are `code`
   and `message`
}

if unwrap .string ~= json { 
   ... use `string` here (same name as enum case) because
   no label was used to define the associated value
}
 <https://gist.github.com/erica/db9ce92b3d23cb20799460f603c0ae7c#impact-on-existing-code>Impact on Existing Code

This proposal is breaking and would require migration.

 <https://gist.github.com/erica/db9ce92b3d23cb20799460f603c0ae7c#alternatives-considered>Alternatives Considered

Leaving the grammar as-is, albeit confusing
Retaining case and replacing the equal sign with ~= (pattern matching) or : (to match the switch statement).

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20161101/6eafbc39/attachment.html>


More information about the swift-evolution mailing list