[swift-evolution] guard let x = x

Nevin Brackett-Rozinsky nevin.brackettrozinsky at gmail.com
Tue Nov 1 17:38:03 CDT 2016


I just realized something we can already do *now* (albeit with a lot of
boilerplate) that looks really sleek at the point of use. For example:

let json = JSON.number(4)

if let n = json.number {
    // n is a Double here
}

guard let s = json.string else {
    return
}

Making this work is actually quite straightforward, and simply requires
extending the enum with a property for each unwrappable case, which returns
an Optional of the associated value or nil if it is a different case.

If we had a way to eliminate the boilerplate, such as an “@unwrappable”
attribute or a magic “Unwrappable” protocol that would cause the complier
to generate the requisite properties, then it might entirely obviate the
need for an “unwrap” keyword, or any change to pattern matching at all.

Here is what it takes to make your JSON enum behave as I describe:

extension JSON {
    var string: String? {
        if case let .string(x) = self { return x }
        return nil
    }

    var number: Double? {
        if case let .number(x) = self { return x }
        return nil
    }

    var boolean: Bool? {
        if case let .boolean(x) = self { return x }
        return nil
    }

    var array: [JSON]? {
        if case let .array(x) = self { return x }
        return nil
    }

    var dictionary: [String: JSON]? {
        if case let .dictionary(x) = self { return x }
        return nil
    }
}

With compiler support this could probably avoid creating the intermediate
Optional altogether.

Nevin


On Tue, Nov 1, 2016 at 4:28 PM, Nevin Brackett-Rozinsky <
nevin.brackettrozinsky at gmail.com> wrote:

> Bikeshedding here—is there any way we can make it look like this for enums?
>
> if let str = unwrap json.string {
>     // str is the value from the .string enum case
> }
>
> Essentially, the “unwrap” keyword would treat the last “path component” of
> what follows as an enum case, and extract the associated value if it
> matches.
>
> This could even work with tuple destructuring, as in:
>
> guard let (theCode, someMessage) = unwrap myResponse.contact else {
>     return
> }
>
> And of course we could add a sugared form for optionals to allow the
> simple, “if unwrap x { … }”.
>
> I’m not sure if this is where we want to take the idea, it’s just
> something I thought of.
>
> If we did go that route, then we might need to disallow enums from having
> properties with the same names as their cases (otherwise, eg. json.string
> would refer to a property not a case).
>
> On the other hand, a slightly different syntax would avoid that issue
> entirely—for instance,
>
> if let str = unwrap json(.string) { … }
> or
> if let str = unwrap(.string) json { … }
>
> That last one could even be sugared further when shadowing is desired:
>
> if unwrap(.string) json {
>     // json is a String in here
> }
>
> Potentially worthwhile?
>
> Nevin
>
>
> On Tue, Nov 1, 2016 at 2:42 PM, Erica Sadun via swift-evolution <
> swift-evolution at swift.org> wrote:
>
>> 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
>>
>> 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 { ... } // redundantguard case let .some(foo) = foo else { ... } // overly complexguard 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 keywordif unwrap myValue { ... }
>> // Existing same-name shadowingif let myValue = myValue { ... }
>> // Existing same-name pattern matching and conditional bindingif case .some(let myValue) = myValue { ... } // old grammarif case let .some(myValue) = myValue { ... } // old grammar// Proposed same-name pattern matching and conditional bindingif .some(let myValue) ~= myValue { ... } // new grammarif 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 { ... } // currentif case let x? = anOptional { ... } // current, would be removedif 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 { ... } // beforeif case range = myValue { ... } // beforeif 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 typeenum Result<Value> { case success(Value), failure(Error) }
>> // Many cases of mixed typesenum JSON {
>>     case string(String)
>>     case number(Double)
>>     case boolean(Bool)
>>     case array([JSON])
>>     case dictionary([String: JSON])
>>     case null
>> }
>> // Multiple associated valuesenum 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 proposedif 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).
>>
>>
>>
>> _______________________________________________
>> swift-evolution mailing list
>> swift-evolution at swift.org
>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>
>>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20161101/12c5c505/attachment.html>


More information about the swift-evolution mailing list