[Pitch] Introducing `Unwrappable` protocol


(Erica Sadun) #1

I have been involved in several separate related draft proposals for discussions
that were cut off about 4 months ago. I believe they meet the criteria for Stage 2,
but I'm doing a poor job presenting them coherently on-list.

Because of that, I'm going to start over here, hopefully pulling in all the details
and allowing the community to provide feedback and direction. The following
gist is an amalgam of work I was discussing with Xiaodi Wu, Chris Lattner, and
David Goodine.

https://gist.github.com/erica/aea6a1c55e9e92f843f92e2b16879b0f

I've decided to link to the gist rather than paste the entire proposal as that never seems to
really work here.

In a nutshell:

Unwrapping values is one of the most common Swift tasks and it is unnecessarily complex.

Consider the following solutions:

Introduce an unwrap keyword for Optional values
Introduce an Unwrappable protocol for associated-value enumerations.
Apply unwrap to non-Optional values.
Extend for and switch.
Fix pattern match binding issues.
Simplify complex binding.
<https://gist.github.com/erica/aea6a1c55e9e92f843f92e2b16879b0f#motivation>Motivation

Unwrapping with conditional binding and pattern matching is unnecessarily complex and dangerous:

Using "foo = foo" fails DRY principles <https://en.wikipedia.org/wiki/Don't_repeat_yourself>.
Using case let .some(foo) = foo or case .some(let foo) = foo fails KISS principles <https://en.wikipedia.org/wiki/KISS_principle>.
Using the = operator fails the Principle of Least Astonishment <https://en.wikipedia.org/wiki/Principle_of_least_astonishment>.
Allowing user-provided names may shadow existing variables without compiler warnings.
The complexity and freedom of let and var placement can introduce bugs in edge cases.
-- E


Introducing `Unwrappable`, a biased unwrapping protocol
(Jon Hull) #2

Would ‘if let foo = foo’ still be allowed?

···

On Mar 7, 2017, at 12:14 PM, Erica Sadun via swift-evolution <swift-evolution@swift.org> wrote:

I have been involved in several separate related draft proposals for discussions
that were cut off about 4 months ago. I believe they meet the criteria for Stage 2,
but I'm doing a poor job presenting them coherently on-list.

Because of that, I'm going to start over here, hopefully pulling in all the details
and allowing the community to provide feedback and direction. The following
gist is an amalgam of work I was discussing with Xiaodi Wu, Chris Lattner, and
David Goodine.

https://gist.github.com/erica/aea6a1c55e9e92f843f92e2b16879b0f

I've decided to link to the gist rather than paste the entire proposal as that never seems to
really work here.

In a nutshell:

Unwrapping values is one of the most common Swift tasks and it is unnecessarily complex.

Consider the following solutions:

Introduce an unwrap keyword for Optional values
Introduce an Unwrappable protocol for associated-value enumerations.
Apply unwrap to non-Optional values.
Extend for and switch.
Fix pattern match binding issues.
Simplify complex binding.
<https://gist.github.com/erica/aea6a1c55e9e92f843f92e2b16879b0f#motivation>Motivation

Unwrapping with conditional binding and pattern matching is unnecessarily complex and dangerous:

Using "foo = foo" fails DRY principles <https://en.wikipedia.org/wiki/Don't_repeat_yourself>.
Using case let .some(foo) = foo or case .some(let foo) = foo fails KISS principles <https://en.wikipedia.org/wiki/KISS_principle>.
Using the = operator fails the Principle of Least Astonishment <https://en.wikipedia.org/wiki/Principle_of_least_astonishment>.
Allowing user-provided names may shadow existing variables without compiler warnings.
The complexity and freedom of let and var placement can introduce bugs in edge cases.
-- E
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Erica Sadun) #3

Would ‘if let foo = foo’ still be allowed?

Existing code should to continue to work with `if let foo = foo`. I don't
believe I put anything in-text about removing this, but if you see something
let me know.

-- E

···

On Mar 7, 2017, at 1:31 PM, Jonathan Hull <jhull@gbis.com> wrote:

On Mar 7, 2017, at 12:14 PM, Erica Sadun via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

I have been involved in several separate related draft proposals for discussions
that were cut off about 4 months ago. I believe they meet the criteria for Stage 2,
but I'm doing a poor job presenting them coherently on-list.

Because of that, I'm going to start over here, hopefully pulling in all the details
and allowing the community to provide feedback and direction. The following
gist is an amalgam of work I was discussing with Xiaodi Wu, Chris Lattner, and
David Goodine.

https://gist.github.com/erica/aea6a1c55e9e92f843f92e2b16879b0f

I've decided to link to the gist rather than paste the entire proposal as that never seems to
really work here.

In a nutshell:

Unwrapping values is one of the most common Swift tasks and it is unnecessarily complex.

Consider the following solutions:

Introduce an unwrap keyword for Optional values
Introduce an Unwrappable protocol for associated-value enumerations.
Apply unwrap to non-Optional values.
Extend for and switch.
Fix pattern match binding issues.
Simplify complex binding.
<https://gist.github.com/erica/aea6a1c55e9e92f843f92e2b16879b0f#motivation>Motivation

Unwrapping with conditional binding and pattern matching is unnecessarily complex and dangerous:

Using "foo = foo" fails DRY principles <https://en.wikipedia.org/wiki/Don't_repeat_yourself>.
Using case let .some(foo) = foo or case .some(let foo) = foo fails KISS principles <https://en.wikipedia.org/wiki/KISS_principle>.
Using the = operator fails the Principle of Least Astonishment <https://en.wikipedia.org/wiki/Principle_of_least_astonishment>.
Allowing user-provided names may shadow existing variables without compiler warnings.
The complexity and freedom of let and var placement can introduce bugs in edge cases.
-- E
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution


(Jaden Geller) #4

It’s worth mentioning that the normal let binding can be used for pattern matching:
  let (a, b, c) = foo()

This nicely parallels the existing case syntax:
  if case let .blah(a, b, c) = bar() { … }
It would feel inconsistent if the order switched when in a conditional binding.

I would prefer that `case` was removed to best mirror the normal syntax, requiring `?` or `.some` to be used for optionals
  if let .blah(a, b, c) = bar() { … }
  if let unwrapped? = wrapped { … }
  if let .some(unwrapped) = wrapped { … }
but I realize this is source-breaking, so I’m happy with the existing syntax.

Unrelated: I feel like a single case [closed] enum shouldn’t require an `if` to match, similar to tuples:
  let .onlyCase(a) = baz()
I’m not sure this is particularly useful, but it seems consistent; if a pattern is irrefutable, a conditional binding is unnecessary.

Anyway, I’m definitely -1 on the idea as a whole. It doesn’t clearly seem cleaner to me, and it is majorly source breaking. I’m +1 for a warning to avoid accidental shadowing—why doesn’t it give an unused variable warning already? Seems odd…

···

On Mar 7, 2017, at 12:14 PM, Erica Sadun via swift-evolution <swift-evolution@swift.org> wrote:

I have been involved in several separate related draft proposals for discussions
that were cut off about 4 months ago. I believe they meet the criteria for Stage 2,
but I'm doing a poor job presenting them coherently on-list.

Because of that, I'm going to start over here, hopefully pulling in all the details
and allowing the community to provide feedback and direction. The following
gist is an amalgam of work I was discussing with Xiaodi Wu, Chris Lattner, and
David Goodine.

https://gist.github.com/erica/aea6a1c55e9e92f843f92e2b16879b0f

I've decided to link to the gist rather than paste the entire proposal as that never seems to
really work here.

In a nutshell:

Unwrapping values is one of the most common Swift tasks and it is unnecessarily complex.

Consider the following solutions:

Introduce an unwrap keyword for Optional values
Introduce an Unwrappable protocol for associated-value enumerations.
Apply unwrap to non-Optional values.
Extend for and switch.
Fix pattern match binding issues.
Simplify complex binding.
<https://gist.github.com/erica/aea6a1c55e9e92f843f92e2b16879b0f#motivation>Motivation

Unwrapping with conditional binding and pattern matching is unnecessarily complex and dangerous:

Using "foo = foo" fails DRY principles <https://en.wikipedia.org/wiki/Don't_repeat_yourself>.
Using case let .some(foo) = foo or case .some(let foo) = foo fails KISS principles <https://en.wikipedia.org/wiki/KISS_principle>.
Using the = operator fails the Principle of Least Astonishment <https://en.wikipedia.org/wiki/Principle_of_least_astonishment>.
Allowing user-provided names may shadow existing variables without compiler warnings.
The complexity and freedom of let and var placement can introduce bugs in edge cases.
-- E
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Brent Royal-Gordon) #5

Treating the things separately:

1. Introduce an `unwrap` keyword

I'm really not convinced this pulls its own weight. Without the `let`, it doesn't make the fact that it's shadowing the original (and thus that you cannot modify it) clear; with the `let`, it introduces a new keyword people need to learn for the sake of eliding a repeated variable name.

In the document, you state that `unwrap` "simplifies the status quo and eleminates unintended shadows", but that's not true, because the existing syntax will continue to exist and be supported. Unless we warn about *any* shadowing in an `if let` or `if case`, it will still be possible to accidentally shadow variables using these declarations.

2. Introduce an `Unwrappable` protocol

I like the idea, but I would use a slightly different design which offers more features and lifts this from "bag of syntax" territory into representing a discrete semantic. This particular design includes several elements which depend on other proposed features:

  /// Conforming types wrap another type, creating a supertype which may or may not
  /// contain the `Wrapped` type.
  ///
  /// `Wrapper` types may use the `!` operator to unconditionally access the wrapped
  /// value or the `if let` and `guard let` statements to conditionally access it. Additionally,
  /// `Wrapped` values will be automatically converted to the `Wrapper`-conforming type
  /// as needed, and the `is`, `as`, `as?`, and `as!` operators will treat the `Wrapped` type
  /// as a subtype of the `Wrapper`-conforming type.
  protocol Wrapper {
    /// The type that this value wraps.
    associatedtype Wrapped
    
    /// The type of error, if any, thrown when a non-wrapped value is unwrapped.
    associatedtype UnwrappingError: Error = Never
    
    /// Creates an instance of `Self` which wraps the `Wrapped` value.
    ///
    /// You can call this initializer explicitly, but Swift will also insert implicit calls when
    /// upcasting from `Wrapped` to `Self`.
    init(_ wrapped: Wrapped)
    
    /// Returns `true` if `Self` contains an instance of `Wrapped` which can be accessed
    /// by calling `unwrapped`.
    var isWrapped: Bool { get }
    
    /// Accesses the `Wrapped` value within this instance.
    ///
    /// If `isWrapped` is `true`, this property will always return an instance. If it is `false`, this property
    /// will throw an instance of `UnwrappingError`, or trap if `UnwrappingError` is `Never`.
    var unwrapped: Wrapped { get throws<UnwrappingError> }
    
    /// Accesses the `Wrapped` value within this instance, possibly skipping safety checks.
    ///
    /// - Precondition: `isWrapped` is `true`.
    var unsafelyUnwrapped: Wrapped { get }
  }
  
  extension Wrapper {
    // Default implementation of `unsafelyUnwrapped` just calls `unwrapped`.
    var unsafelyUnwrapped: Wrapped {
      return try! unwrapped
    }
  }

The defaulting of `WrappingError` to `Never` means the error-emitting aspects of this design are additive and can be introduced later, once the necessary supporting features are introduced. The use of separate `isWrapped` and `unwrapped` properties means that `unwrapped` can implement an appropriate behavior on unwrapping failure, instead of being forced to return `nil`.

(An alternative design would have `wrapped: Wrapped? { get }` and `unwrapped: Wrapped { get throws<UnwrappingError> }` properties, instead of `isWrapped` and `unwrapped`.)

In this model, your example of:

  let value = try unwrap myResult // throws on `failure`

Would instead be:

  let value = try myResult! // throws on `failure`

(Actually, I'm not sure why you said this would be `unwrap`—it's not shadowing `myResult`, is it?)

Theoretically, this exact design—or something close to it—could be used to implement subtyping:

  extension Int16: Wrapper {
    typealias Wrapped = Int8
    
    init(_ wrapped: Int8) {
      self.init(exactly: wrapped)!
    }
    
    var isWrapped: Bool {
      return Self(exactly: Int8.min)...Self(exactly: Int8.max).contains(self)
    }
    
    var unwrapped: Int8 {
      return Self(exactly: self)!
    }
  }

But this would imply that you could not only say `myInt8` where an `Int16` was needed, but also that you could write `myInt16!` where an `Int8` was needed. I'm not sure we want to overload force unwrapping like that. One possibility is that unwrapping is a refinement of subtyping:

  // `Downcastable` contains the actual conversion and subtyping logic. Conforming to
  // `Downcastable` gets you `is`, `as`, `as?`, and `as!` support; it also lets you use an
  // instance of `Subtype` in contexts which want a `Supertype`.
  protocol Downcastable {
    associatedtype Subtype
    associatedtype DowncastingError: Error = Never
    
    init(upcasting subvalue: Subtype)
    
    var canDowncast: Bool { get }
    
    var downcasted: Subtype { get throws<DowncastingError> }
    
    var unsafelyDowncasted: Subtype { get }
  }
  
  // Unwrappable refines Downcastable, providing access to `!`, `if let`, etc.
  protocol Unwrappable: Downcastable {}
  extension Unwrappable {
    var unsafelyUnwrapped: Subtype { return unsafelyDowncasted }
  }

That would allow you to have conversions between `Int8` and `Int16`, but not to use `!` on an `Int16`.

3. Apply `unwrap` to non-`Optional` values, and
4. Extend `for` and `switch`

These are pretty straightforward ramifications of having both `unwrap` and `Unwrappable`. I don't like `unwrap`, but if we *do* add it, it should certainly do this.

5. Fix Pattern Match Binding

The `case let .someCase(x, y)` syntax is really convenient when there are a lot of variables to bind. I would suggest a fairly narrow warning: If you use a leading `let`, and some—but not all—of the variables bound by the pattern are shadowing, emit a warning. That would solve the `case let .two(newValue, oldValue)`-where-`oldValue`-should-be-a-match problem.

6. Simplify Complex Binding

I'm not convinced by this. The `case` keyword provides a strong link between `if case` and `switch`/`case`; the `~=` operator doesn't do this. Unless we wanted to redesign `switch`/`case` with matching ergonomics—which, uh, we don't:

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

—I don't think we should go in this direction. `for case` also has similar concerns.

I think we'd be better off replacing the `~=` operator with something more memorable. For instance:

  extension Range {
    public func matches(_ value: Bound) -> Bool {
      return contains(value)
    }
  }

Or:

  public func isMatch<Bound: Comparable>(_ value: Bound, toCase pattern: Range<Bound>) -> Bool {
    return pattern.contains(value)
  }

···

On Mar 7, 2017, at 12:14 PM, Erica Sadun via swift-evolution <swift-evolution@swift.org> wrote:

Because of that, I'm going to start over here, hopefully pulling in all the details
and allowing the community to provide feedback and direction. The following
gist is an amalgam of work I was discussing with Xiaodi Wu, Chris Lattner, and
David Goodine.

https://gist.github.com/erica/aea6a1c55e9e92f843f92e2b16879b0f

--
Brent Royal-Gordon
Architechies


(Guillaume Lessard) #6

I like some of this, but as a single proposal, it does too many things at once.

- The `case let .some(foo)` vs. `case .some(let foo)` issue could be a targeted proposal.
(I really like this)

- The Unwrappable protocol (and keyword) is interesting; it can probably be its own discussion.
(feels unnecessary, without feeling wrong)

- I’m not clear about the proposed solution about shadowing. Is it’s simply the `let` restriction or is there more to it? In any case I don’t think the compiler should disallow shadowing, even if there is a new warning; if there is a new warning there must be a way to spell a shadowed name so as to silence the warning.

- The whole pattern-binding in if/guard/for statements is certainly ripe for improvement; I agree with Anton Zhilin’s comment that pattern-before-expression is confusing. The switch statement puts the expression before the patterns, and it reads well. More than once, after having written an `if case` statement, I later changed it to a `switch` statement in order to improve readability.

Cheers,
Guillaume Lessard


(Greg Parker) #7

We tried `if let unwrapped? = wrapped` some time ago. It was unbelievably unpopular. We changed it back.

···

On Mar 7, 2017, at 3:49 PM, Jaden Geller via swift-evolution <swift-evolution@swift.org> wrote:

It’s worth mentioning that the normal let binding can be used for pattern matching:
  let (a, b, c) = foo()

This nicely parallels the existing case syntax:
  if case let .blah(a, b, c) = bar() { … }
It would feel inconsistent if the order switched when in a conditional binding.

I would prefer that `case` was removed to best mirror the normal syntax, requiring `?` or `.some` to be used for optionals
  if let .blah(a, b, c) = bar() { … }
  if let unwrapped? = wrapped { … }
  if let .some(unwrapped) = wrapped { … }
but I realize this is source-breaking, so I’m happy with the existing syntax.

--
Greg Parker gparker@apple.com <mailto:gparker@apple.com> Runtime Wrangler


(Jaden Geller) #8

Because of that, I'm going to start over here, hopefully pulling in all the details
and allowing the community to provide feedback and direction. The following
gist is an amalgam of work I was discussing with Xiaodi Wu, Chris Lattner, and
David Goodine.

https://gist.github.com/erica/aea6a1c55e9e92f843f92e2b16879b0f

Treating the things separately:

1. Introduce an `unwrap` keyword

I'm really not convinced this pulls its own weight. Without the `let`, it doesn't make the fact that it's shadowing the original (and thus that you cannot modify it) clear; with the `let`, it introduces a new keyword people need to learn for the sake of eliding a repeated variable name.

If Swift supported `if inout x = x { … }`, then `unwrap` could have much more reasonable semantics.

···

On Mar 7, 2017, at 8:59 PM, Brent Royal-Gordon via swift-evolution <swift-evolution@swift.org> wrote:

On Mar 7, 2017, at 12:14 PM, Erica Sadun via swift-evolution <swift-evolution@swift.org> wrote:

In the document, you state that `unwrap` "simplifies the status quo and eleminates unintended shadows", but that's not true, because the existing syntax will continue to exist and be supported. Unless we warn about *any* shadowing in an `if let` or `if case`, it will still be possible to accidentally shadow variables using these declarations.

2. Introduce an `Unwrappable` protocol

I like the idea, but I would use a slightly different design which offers more features and lifts this from "bag of syntax" territory into representing a discrete semantic. This particular design includes several elements which depend on other proposed features:

  /// Conforming types wrap another type, creating a supertype which may or may not
  /// contain the `Wrapped` type.
  ///
  /// `Wrapper` types may use the `!` operator to unconditionally access the wrapped
  /// value or the `if let` and `guard let` statements to conditionally access it. Additionally,
  /// `Wrapped` values will be automatically converted to the `Wrapper`-conforming type
  /// as needed, and the `is`, `as`, `as?`, and `as!` operators will treat the `Wrapped` type
  /// as a subtype of the `Wrapper`-conforming type.
  protocol Wrapper {
    /// The type that this value wraps.
    associatedtype Wrapped
    
    /// The type of error, if any, thrown when a non-wrapped value is unwrapped.
    associatedtype UnwrappingError: Error = Never
    
    /// Creates an instance of `Self` which wraps the `Wrapped` value.
    ///
    /// You can call this initializer explicitly, but Swift will also insert implicit calls when
    /// upcasting from `Wrapped` to `Self`.
    init(_ wrapped: Wrapped)
    
    /// Returns `true` if `Self` contains an instance of `Wrapped` which can be accessed
    /// by calling `unwrapped`.
    var isWrapped: Bool { get }
    
    /// Accesses the `Wrapped` value within this instance.
    ///
    /// If `isWrapped` is `true`, this property will always return an instance. If it is `false`, this property
    /// will throw an instance of `UnwrappingError`, or trap if `UnwrappingError` is `Never`.
    var unwrapped: Wrapped { get throws<UnwrappingError> }
    
    /// Accesses the `Wrapped` value within this instance, possibly skipping safety checks.
    ///
    /// - Precondition: `isWrapped` is `true`.
    var unsafelyUnwrapped: Wrapped { get }
  }
  
  extension Wrapper {
    // Default implementation of `unsafelyUnwrapped` just calls `unwrapped`.
    var unsafelyUnwrapped: Wrapped {
      return try! unwrapped
    }
  }

The defaulting of `WrappingError` to `Never` means the error-emitting aspects of this design are additive and can be introduced later, once the necessary supporting features are introduced. The use of separate `isWrapped` and `unwrapped` properties means that `unwrapped` can implement an appropriate behavior on unwrapping failure, instead of being forced to return `nil`.

(An alternative design would have `wrapped: Wrapped? { get }` and `unwrapped: Wrapped { get throws<UnwrappingError> }` properties, instead of `isWrapped` and `unwrapped`.)

In this model, your example of:

  let value = try unwrap myResult // throws on `failure`

Would instead be:

  let value = try myResult! // throws on `failure`

(Actually, I'm not sure why you said this would be `unwrap`—it's not shadowing `myResult`, is it?)

Theoretically, this exact design—or something close to it—could be used to implement subtyping:

  extension Int16: Wrapper {
    typealias Wrapped = Int8
    
    init(_ wrapped: Int8) {
      self.init(exactly: wrapped)!
    }
    
    var isWrapped: Bool {
      return Self(exactly: Int8.min)...Self(exactly: Int8.max).contains(self)
    }
    
    var unwrapped: Int8 {
      return Self(exactly: self)!
    }
  }

But this would imply that you could not only say `myInt8` where an `Int16` was needed, but also that you could write `myInt16!` where an `Int8` was needed. I'm not sure we want to overload force unwrapping like that. One possibility is that unwrapping is a refinement of subtyping:

  // `Downcastable` contains the actual conversion and subtyping logic. Conforming to
  // `Downcastable` gets you `is`, `as`, `as?`, and `as!` support; it also lets you use an
  // instance of `Subtype` in contexts which want a `Supertype`.
  protocol Downcastable {
    associatedtype Subtype
    associatedtype DowncastingError: Error = Never
    
    init(upcasting subvalue: Subtype)
    
    var canDowncast: Bool { get }
    
    var downcasted: Subtype { get throws<DowncastingError> }
    
    var unsafelyDowncasted: Subtype { get }
  }
  
  // Unwrappable refines Downcastable, providing access to `!`, `if let`, etc.
  protocol Unwrappable: Downcastable {}
  extension Unwrappable {
    var unsafelyUnwrapped: Subtype { return unsafelyDowncasted }
  }

That would allow you to have conversions between `Int8` and `Int16`, but not to use `!` on an `Int16`.

3. Apply `unwrap` to non-`Optional` values, and
4. Extend `for` and `switch`

These are pretty straightforward ramifications of having both `unwrap` and `Unwrappable`. I don't like `unwrap`, but if we *do* add it, it should certainly do this.

5. Fix Pattern Match Binding

The `case let .someCase(x, y)` syntax is really convenient when there are a lot of variables to bind. I would suggest a fairly narrow warning: If you use a leading `let`, and some—but not all—of the variables bound by the pattern are shadowing, emit a warning. That would solve the `case let .two(newValue, oldValue)`-where-`oldValue`-should-be-a-match problem.

6. Simplify Complex Binding

I'm not convinced by this. The `case` keyword provides a strong link between `if case` and `switch`/`case`; the `~=` operator doesn't do this. Unless we wanted to redesign `switch`/`case` with matching ergonomics—which, uh, we don't:

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

—I don't think we should go in this direction. `for case` also has similar concerns.

I think we'd be better off replacing the `~=` operator with something more memorable. For instance:

  extension Range {
    public func matches(_ value: Bound) -> Bool {
      return contains(value)
    }
  }

Or:

  public func isMatch<Bound: Comparable>(_ value: Bound, toCase pattern: Range<Bound>) -> Bool {
    return pattern.contains(value)
  }

--
Brent Royal-Gordon
Architechies

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(David Hart) #9

I will attach my comments to Brent’s answer because I echo many of his thoughts:

Because of that, I'm going to start over here, hopefully pulling in all the details
and allowing the community to provide feedback and direction. The following
gist is an amalgam of work I was discussing with Xiaodi Wu, Chris Lattner, and
David Goodine.

https://gist.github.com/erica/aea6a1c55e9e92f843f92e2b16879b0f

Treating the things separately:

1. Introduce an `unwrap` keyword

I'm really not convinced this pulls its own weight. Without the `let`, it doesn't make the fact that it's shadowing the original (and thus that you cannot modify it) clear; with the `let`, it introduces a new keyword people need to learn for the sake of eliding a repeated variable name.

In the document, you state that `unwrap` "simplifies the status quo and eleminates unintended shadows", but that's not true, because the existing syntax will continue to exist and be supported. Unless we warn about *any* shadowing in an `if let` or `if case`, it will still be possible to accidentally shadow variables using these declarations.

I’m also not convinced this is worth it. While I see one argument for this feature (following DRY principles) I see many disadvantages:

introduction of an extra keyword
newcomers have to learn that there are two different syntaxes for optional binding
less clear that shadowing is happening

2. Introduce an `Unwrappable` protocol

I like the idea, but I would use a slightly different design which offers more features and lifts this from "bag of syntax" territory into representing a discrete semantic. This particular design includes several elements which depend on other proposed features:

  /// Conforming types wrap another type, creating a supertype which may or may not
  /// contain the `Wrapped` type.
  ///
  /// `Wrapper` types may use the `!` operator to unconditionally access the wrapped
  /// value or the `if let` and `guard let` statements to conditionally access it. Additionally,
  /// `Wrapped` values will be automatically converted to the `Wrapper`-conforming type
  /// as needed, and the `is`, `as`, `as?`, and `as!` operators will treat the `Wrapped` type
  /// as a subtype of the `Wrapper`-conforming type.
  protocol Wrapper {
    /// The type that this value wraps.
    associatedtype Wrapped
    
    /// The type of error, if any, thrown when a non-wrapped value is unwrapped.
    associatedtype UnwrappingError: Error = Never
    
    /// Creates an instance of `Self` which wraps the `Wrapped` value.
    ///
    /// You can call this initializer explicitly, but Swift will also insert implicit calls when
    /// upcasting from `Wrapped` to `Self`.
    init(_ wrapped: Wrapped)
    
    /// Returns `true` if `Self` contains an instance of `Wrapped` which can be accessed
    /// by calling `unwrapped`.
    var isWrapped: Bool { get }
    
    /// Accesses the `Wrapped` value within this instance.
    ///
    /// If `isWrapped` is `true`, this property will always return an instance. If it is `false`, this property
    /// will throw an instance of `UnwrappingError`, or trap if `UnwrappingError` is `Never`.
    var unwrapped: Wrapped { get throws<UnwrappingError> }
    
    /// Accesses the `Wrapped` value within this instance, possibly skipping safety checks.
    ///
    /// - Precondition: `isWrapped` is `true`.
    var unsafelyUnwrapped: Wrapped { get }
  }
  
  extension Wrapper {
    // Default implementation of `unsafelyUnwrapped` just calls `unwrapped`.
    var unsafelyUnwrapped: Wrapped {
      return try! unwrapped
    }
  }

The defaulting of `WrappingError` to `Never` means the error-emitting aspects of this design are additive and can be introduced later, once the necessary supporting features are introduced. The use of separate `isWrapped` and `unwrapped` properties means that `unwrapped` can implement an appropriate behavior on unwrapping failure, instead of being forced to return `nil`.

(An alternative design would have `wrapped: Wrapped? { get }` and `unwrapped: Wrapped { get throws<UnwrappingError> }` properties, instead of `isWrapped` and `unwrapped`.)

In this model, your example of:

  let value = try unwrap myResult // throws on `failure`

Would instead be:

  let value = try myResult! // throws on `failure`

Wouldn’t this be confusing for people who associate ! with crashing behaviour (in optional! and try!)

(Actually, I'm not sure why you said this would be `unwrap`—it's not shadowing `myResult`, is it?)

Theoretically, this exact design—or something close to it—could be used to implement subtyping:

  extension Int16: Wrapper {
    typealias Wrapped = Int8
    
    init(_ wrapped: Int8) {
      self.init(exactly: wrapped)!
    }
    
    var isWrapped: Bool {
      return Self(exactly: Int8.min)...Self(exactly: Int8.max).contains(self)
    }
    
    var unwrapped: Int8 {
      return Self(exactly: self)!
    }
  }

But this would imply that you could not only say `myInt8` where an `Int16` was needed, but also that you could write `myInt16!` where an `Int8` was needed. I'm not sure we want to overload force unwrapping like that. One possibility is that unwrapping is a refinement of subtyping:

  // `Downcastable` contains the actual conversion and subtyping logic. Conforming to
  // `Downcastable` gets you `is`, `as`, `as?`, and `as!` support; it also lets you use an
  // instance of `Subtype` in contexts which want a `Supertype`.
  protocol Downcastable {
    associatedtype Subtype
    associatedtype DowncastingError: Error = Never
    
    init(upcasting subvalue: Subtype)
    
    var canDowncast: Bool { get }
    
    var downcasted: Subtype { get throws<DowncastingError> }
    
    var unsafelyDowncasted: Subtype { get }
  }
  
  // Unwrappable refines Downcastable, providing access to `!`, `if let`, etc.
  protocol Unwrappable: Downcastable {}
  extension Unwrappable {
    var unsafelyUnwrapped: Subtype { return unsafelyDowncasted }
  }

That would allow you to have conversions between `Int8` and `Int16`, but not to use `!` on an `Int16`.

3. Apply `unwrap` to non-`Optional` values, and
4. Extend `for` and `switch`

These are pretty straightforward ramifications of having both `unwrap` and `Unwrappable`. I don't like `unwrap`, but if we *do* add it, it should certainly do this.

Same arguments as for (1)

5. Fix Pattern Match Binding

The `case let .someCase(x, y)` syntax is really convenient when there are a lot of variables to bind. I would suggest a fairly narrow warning: If you use a leading `let`, and some—but not all—of the variables bound by the pattern are shadowing, emit a warning. That would solve the `case let .two(newValue, oldValue)`-where-`oldValue`-should-be-a-match problem.

Agreed. The leading let is worth it when matching many variables! I also prefer the warning solution. Forcing let at the variable site would defeat DRY principles.

6. Simplify Complex Binding

I'm not convinced by this. The `case` keyword provides a strong link between `if case` and `switch`/`case`; the `~=` operator doesn't do this. Unless we wanted to redesign `switch`/`case` with matching ergonomics—which, uh, we don't:

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

—I don't think we should go in this direction. `for case` also has similar concerns.

I think we'd be better off replacing the `~=` operator with something more memorable. For instance:

  extension Range {
    public func matches(_ value: Bound) -> Bool {
      return contains(value)
    }
  }

Or:

  public func isMatch<Bound: Comparable>(_ value: Bound, toCase pattern: Range<Bound>) -> Bool {
    return pattern.contains(value)
  }

And here’s the only place I disagree with Brent. The way I see it, the `case` keyword does not provide a strong link between `if case` and `switch`/`case`, it simply confuses people who do not expect the see that keyword associated with anything else than switch. It does not scream “pattern matching” to me like ~= does.

Erica, I think that section could contain more examples of the proposed syntax being used with cases:

if .left(let x) ~= either { … }

···

On 8 Mar 2017, at 05:59, Brent Royal-Gordon via swift-evolution <swift-evolution@swift.org> wrote:

On Mar 7, 2017, at 12:14 PM, Erica Sadun via swift-evolution <swift-evolution@swift.org> wrote:

--
Brent Royal-Gordon
Architechies

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(David Hart) #10

Would ‘if let foo = foo’ still be allowed?

Existing code should to continue to work with `if let foo = foo`. I don't
believe I put anything in-text about removing this, but if you see something
let me know.

But there’s nothing in-text that states clearly that you are not removing the old syntax. I think it would be worth clarifying.

···

On 7 Mar 2017, at 21:41, Erica Sadun via swift-evolution <swift-evolution@swift.org> wrote:

On Mar 7, 2017, at 1:31 PM, Jonathan Hull <jhull@gbis.com <mailto:jhull@gbis.com>> wrote:

-- E

On Mar 7, 2017, at 12:14 PM, Erica Sadun via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

I have been involved in several separate related draft proposals for discussions
that were cut off about 4 months ago. I believe they meet the criteria for Stage 2,
but I'm doing a poor job presenting them coherently on-list.

Because of that, I'm going to start over here, hopefully pulling in all the details
and allowing the community to provide feedback and direction. The following
gist is an amalgam of work I was discussing with Xiaodi Wu, Chris Lattner, and
David Goodine.

https://gist.github.com/erica/aea6a1c55e9e92f843f92e2b16879b0f

I've decided to link to the gist rather than paste the entire proposal as that never seems to
really work here.

In a nutshell:

Unwrapping values is one of the most common Swift tasks and it is unnecessarily complex.

Consider the following solutions:

Introduce an unwrap keyword for Optional values
Introduce an Unwrappable protocol for associated-value enumerations.
Apply unwrap to non-Optional values.
Extend for and switch.
Fix pattern match binding issues.
Simplify complex binding.
<https://gist.github.com/erica/aea6a1c55e9e92f843f92e2b16879b0f#motivation>Motivation

Unwrapping with conditional binding and pattern matching is unnecessarily complex and dangerous:

Using "foo = foo" fails DRY principles <https://en.wikipedia.org/wiki/Don't_repeat_yourself>.
Using case let .some(foo) = foo or case .some(let foo) = foo fails KISS principles <https://en.wikipedia.org/wiki/KISS_principle>.
Using the = operator fails the Principle of Least Astonishment <https://en.wikipedia.org/wiki/Principle_of_least_astonishment>.
Allowing user-provided names may shadow existing variables without compiler warnings.
The complexity and freedom of let and var placement can introduce bugs in edge cases.
-- E
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Erica Sadun) #11

I like some of this, but as a single proposal, it does too many things at once.

I deliberately moved it out of proposal format for that reason, so it could be discussed first.

- The `case let .some(foo)` vs. `case .some(let foo)` issue could be a targeted proposal.
(I really like this)

I put it in as a bug report (link in repo) based on ?Anton's? suggestion but Jordan says this
isn't similar to normal warnings they emit.

- The Unwrappable protocol (and keyword) is interesting; it can probably be its own discussion.
(feels unnecessary, without feeling wrong)

Unwrappable is driven from Chris L to simplify language implementation and support
the eventual introduction of Result/Either. If that's not needed anymore, I'm sure
he or others will let me know and I'll cross out. I love the idea of an easy way
to use existing syntax sugar for custom types.

- I’m not clear about the proposed solution about shadowing. Is it’s simply the `let` restriction or is there more to it? In any case I don’t think the compiler should disallow shadowing, even if there is a new warning; if there is a new warning there must be a way to spell a shadowed name so as to silence the warning.

Several points touch on shadowing:

* creating something more readable and safe with `unwrap`
* removing a danger that cannot be addressed with `unwrap` for multi-associated values

- The whole pattern-binding in if/guard/for statements is certainly ripe for improvement; I agree with Anton Zhilin’s comment that pattern-before-expression is confusing. The switch statement puts the expression before the patterns, and it reads well. More than once, after having written an `if case` statement, I later changed it to a `switch` statement in order to improve readability.

I have no problem switching them and doing =~ vs ~= but I remember this point being
brought up and there was a clear design decision to do pattern first value second. I'll
defer that to anyone who knows the background.

-- E

···

On Mar 7, 2017, at 3:44 PM, Guillaume Lessard via swift-evolution <swift-evolution@swift.org> wrote:

Cheers,
Guillaume Lessard


(Derrick Ho) #12

I disagree that the following is better

guard unwrap foo else { ... } // simpler?

It feels like you are re-using foo which previously was an optional but now
is something else. If a variable is a cup, then you'd be reusing a cup that
previously had a different drink in it.

guard let foo = foo else { ... } // current

The current version is preferred since it looks like you are using a new
"cup" to store your non-optional. I like that it is explicit.

···

On Tue, Mar 7, 2017 at 9:27 PM Greg Parker via swift-evolution < swift-evolution@swift.org> wrote:

On Mar 7, 2017, at 3:49 PM, Jaden Geller via swift-evolution < > swift-evolution@swift.org> wrote:

It’s worth mentioning that the normal let binding can be used for pattern
matching:
  let (a, b, c) = foo()

This nicely parallels the existing case syntax:
  if case let .blah(a, b, c) = bar() { … }
It would feel inconsistent if the order switched when in a conditional
binding.

I would prefer that `case` was removed to best mirror the normal syntax,
requiring `?` or `.some` to be used for optionals
  if let .blah(a, b, c) = bar() { … }
  if let unwrapped? = wrapped { … }
  if let .some(unwrapped) = wrapped { … }
but I realize this is source-breaking, so I’m happy with the existing
syntax.

We tried `if let unwrapped? = wrapped` some time ago. It was unbelievably
unpopular. We changed it back.

--
Greg Parker gparker@apple.com Runtime Wrangler

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(David Hart) #13

It’s worth mentioning that the normal let binding can be used for pattern matching:
  let (a, b, c) = foo()

This nicely parallels the existing case syntax:
  if case let .blah(a, b, c) = bar() { … }
It would feel inconsistent if the order switched when in a conditional binding.

I would prefer that `case` was removed to best mirror the normal syntax, requiring `?` or `.some` to be used for optionals
  if let .blah(a, b, c) = bar() { … }
  if let unwrapped? = wrapped { … }
  if let .some(unwrapped) = wrapped { … }
but I realize this is source-breaking, so I’m happy with the existing syntax.

We tried `if let unwrapped? = wrapped` some time ago. It was unbelievably unpopular. We changed it back.

Who was it unpopular with? We’re talking about people inside Apple before Swift was released, right?

···

On 8 Mar 2017, at 03:27, Greg Parker via swift-evolution <swift-evolution@swift.org> wrote:

On Mar 7, 2017, at 3:49 PM, Jaden Geller via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

--
Greg Parker gparker@apple.com <mailto:gparker@apple.com> Runtime Wrangler

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Jordan Rose) #14

To be clear, I just didn't want it to be treated as an obvious new warning. It's something worth discussing, and may be a valuable warning (in some form) independent of the general shadowing issue.

Jordan

···

On Mar 7, 2017, at 15:30, Erica Sadun <erica@ericasadun.com> wrote:

- The `case let .some(foo)` vs. `case .some(let foo)` issue could be a targeted proposal.
(I really like this)

I put it in as a bug report (link in repo) based on ?Anton's? suggestion but Jordan says this
isn't similar to normal warnings they emit.


(Guillaume Lessard) #15

I deliberately moved it out of proposal format for that reason, so it could be discussed first.

I see now.

- The `case let .some(foo)` vs. `case .some(let foo)` issue could be a targeted proposal.
(I really like this)

I put it in as a bug report (link in repo) based on ?Anton's? suggestion but Jordan says this
isn't similar to normal warnings they emit.

- The Unwrappable protocol (and keyword) is interesting; it can probably be its own discussion.
(feels unnecessary, without feeling wrong)

Unwrappable is driven from Chris L to simplify language implementation and support
the eventual introduction of Result/Either. If that's not needed anymore, I'm sure
he or others will let me know and I'll cross out. I love the idea of an easy way
to use existing syntax sugar for custom types.

I like it too, but I’m not sure how useful it would really be.

Thanks for the clarification!
Guillaume

···

On Mar 7, 2017, at 4:30 PM, Erica Sadun <erica@ericasadun.com> wrote:


(Charlie Monroe) #16

I deliberately moved it out of proposal format for that reason, so it could be discussed first.

I see now.

- The `case let .some(foo)` vs. `case .some(let foo)` issue could be a targeted proposal.
(I really like this)

I put it in as a bug report (link in repo) based on ?Anton's? suggestion but Jordan says this
isn't similar to normal warnings they emit.

- The Unwrappable protocol (and keyword) is interesting; it can probably be its own discussion.
(feels unnecessary, without feeling wrong)

Unwrappable is driven from Chris L to simplify language implementation and support
the eventual introduction of Result/Either. If that's not needed anymore, I'm sure
he or others will let me know and I'll cross out. I love the idea of an easy way
to use existing syntax sugar for custom types.

I like it too, but I’m not sure how useful it would really be.

Very much for API calls where the callback either has an enum or a callback that has two optionals - return value and an error.

I am definitely +1 on this proposal.

···

On Mar 8, 2017, at 2:33 AM, Guillaume Lessard via swift-evolution <swift-evolution@swift.org> wrote:

On Mar 7, 2017, at 4:30 PM, Erica Sadun <erica@ericasadun.com> wrote:

Thanks for the clarification!
Guillaume

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution