> On Mar 7, 2017, at 12:14 PM, Erica Sadun via swift-evolution
> <[email protected]> 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
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)
}
--
Brent Royal-Gordon
Architechies
_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution