[swift-evolution] [Proposal] Type Narrowing

Nevin Brackett-Rozinsky nevin.brackettrozinsky at gmail.com
Thu Nov 3 14:23:16 CDT 2016


This looks like a lot of complexity for very little gain.

Aside from any implementation concerns, this proposal substantially
increases the cognitive load on developers. To figure out what a piece of
code means, someone reading it will have to mentally keep track of a “type
stack” for every variable. That is the opposite of “clarity at the point of
use”.

For the motivating examples, there is already a much cleaner solution:

(foo as? B)?.someMethodSpecificToB()

Even in the case where foo gets passed as an argument to a function that
takes a B, so optional chaining is not available, we are still okay. If foo
is an instance of a struct, then passing it to a function won’t change its
value so we can write:

if let newFoo = foo as? B {
    funcThatTakesB(newFoo)
}

And if foo is an instance of a class, then the same thing *still* works,
because any changes the function makes to newFoo are also observed in foo
since they reference the same object.

• • •

The proposal’s other example is actually a great illustration of why
shadowing is undesirable. After all, simply binding to a different name
solves the so-called problem, *and* lets us bind to a constant:

var foo: A? = A()
if let newFoo = foo {
    newFoo.someMethod()
    foo!.someMutatingMethod()  // Works now!
}

Full disclosure: both this and the original example have a *deeper*
problem, which is that they are not thread-safe. If foo is modified between
the conditional binding and the force-unwrap, the force-unwrap could fail.
Sure, that can’t happen when foo is a local variable like this, but if it
were a property of a class then it could.

Moreover, the proposed “solution” has the same concurrency failure, but
hides it even worse because the force-unwrap operator doesn’t even appear
in the code:

var foo:A? = A()
if foo != nil {
   foo.someMethod()          // Bad news!
   foo.someMutatingMethod()  // Bad news!
}

In summary, I do not see sufficient motivation for this proposal. The
motivation which I do see appears superfluous. The problem being described
already has an elegant solution in Swift. And the proposed changes
introduce an exceptionally taxing cognitive burden on developers just to
figure out what the type of a variable is on any given line of code.

Plus, in the face of concurrency, such implicit type-narrowing has the
potential to hide serious issues.

Nevin


On Thu, Nov 3, 2016 at 1:04 PM, Haravikk via swift-evolution <
swift-evolution at swift.org> wrote:

> To avoid hijacking the guard let x = x thread entirely I've decided to try
> to write up a proposal on type narrowing in Swift.
> Please give your feedback on the functionality proposed, as well as the
> clarity of the proposal/examples themselves; I've tried to keep it
> straightforward, but I do tend towards being overly verbose, I've always
> tried to have the examples build upon one another to show how it all stacks
> up.
>
>
>
> Type Narrowing
>
>    - Proposal: SE-NNNN
>    <https://github.com/Haravikk/swift-evolution/blob/master/proposals/NNNN-type-narrowing.md>
>    - Author: Haravikk <https://github.com/haravikk>
>    - Status: Awaiting review
>    - Review manager: TBD
>
>
> <https://github.com/Haravikk/swift-evolution/tree/master/proposals#introduction>
> Introduction
>
> This proposal is to introduce type-narrowing to Swift, enabling the
> type-checker to automatically infer a narrower type from context such as
> conditionals.
>
> Swift-evolution thread: Discussion thread topic for that proposal
> <http://news.gmane.org/gmane.comp.lang.swift.evolution>
>
> <https://github.com/Haravikk/swift-evolution/tree/master/proposals#motivation>
> Motivation
>
> Currently in Swift there are various pieces of boilerplate required in
> order to manually narrow types. The most obvious is in the case of
> polymorphism:
>
> let foo:A = B() // B extends A
> if foo is B {
>     (foo as B).someMethodSpecificToB()
> }
>
> But also in the case of unwrapping of optionals:
>
> var foo:A? = A()
> if var foo = foo { // foo is now unwrapped and shadowed
>     foo.someMethod()
>     foo!.someMutatingMethod() // Can't be done
> }
>
>
> <https://github.com/Haravikk/swift-evolution/tree/master/proposals#proposed-solution>Proposed
> solution
>
> The proposed solution to the boiler-plate is to introduce type-narrowing,
> essentially a finer grained knowledge of type based upon context. Thus as
> any contextual clue indicating a more or less specific type are
> encountered, the type of the variable will reflect this from that point
> onwards.
>
> <https://github.com/Haravikk/swift-evolution/tree/master/proposals#detailed-design>Detailed
> design
>
> The concept of type-narrowing would essentially treat all variables as
> having not just a single type, but instead as having a stack of
> increasingly specific (narrow) types.
>
> Whenever a contextual clue such as a conditional is encountered, the type
> checker will infer whether this narrows the type, and add the new narrow
> type to the stack from that point onwards. Whenever the type widens again
> narrower types are popped from the stack.
>
> Here are the above examples re-written to take advantage of type-narrowing:
>
> let foo:A = B() // B extends A
> if foo is B { // B is added to foo's type stack
>     foo.someMethodSpecificToB()
> }
> // B is popped from foo's type stack
>
> var foo:A? = A()
> if foo != nil { // Optional<A>.some is added to foo's type stack
>    foo.someMethod()
>    foo.someMutatingMethod() // Can modify mutable original
> }
> // Optional<A>.some is popped from foo's type stack
>
>
> <https://github.com/Haravikk/swift-evolution/tree/master/proposals#enum-types>Enum
> Types
>
> As seen in the simple optional example, to implement optional support each
> case in an enum is considered be a unique sub-type of the enum itself,
> thus allowing narrowing to nil (.none) and non-nil (.some) types.
>
> This behaviour actually enables some other useful behaviours,
> specifically, if a value is known to be either nil or non-nil then the
> need to unwrap or force unwrap the value can be eliminated entirely, with
> the compiler able to produce errors if these are used incorrectly, for
> example:
>
> var foo:A? = A()
> foo.someMethod() // A is non-nil, no operators required!
> foo = nil
> foo!.someMethod() // Error: foo is always nil at this point
>
> However, unwrapping of the value is only possible if the case contains
> either no value at all, or contains a single value able to satisfy the
> variable's original type requirements. In other words, the value stored in
> Optional<A>.some satisfies the type requirements of var foo:A?, thus it
> is implicitly unwrapped for use. For general enums this likely means no
> cases are implicitly unwrapped unless using a type of Any.
>
> <https://github.com/Haravikk/swift-evolution/tree/master/proposals#type-widening>Type
> Widening
>
> In some cases a type may be narrowed, only to be used in a way that makes
> no sense for the narrowed type. In cases such as these the operation is
> tested against each type in the stack to determine whether the type must
> instead be widened. If a widened type is found it is selected (with
> re-narrowing where possible) otherwise an error is produced as normal.
>
> For example:
>
> let foo:A? = A()
> if (foo != nil) { // Type of foo is Optional<A>.some
>     foo.someMethod()
>     foo = nil // Type of foo is widened to Optional<A>, then re-narrowed to Optional<A>.none
> } // Type of foo is Optional<A>.none
> foo.someMethod() // Error: foo is always nil at this point
>
>
> <https://github.com/Haravikk/swift-evolution/tree/master/proposals#multiple-conditions-and-branching>Multiple
> Conditions and Branching
>
> When dealing with complex conditionals or branches, all paths must agree
> on a common type for narrowing to occur. For example:
>
> let foo:A? = B() // B extends A
> let bar:C = C() // C extends B
>
> if (foo != nil) || (foo == bar) { // Optional<A>.some is added to foo's type stack
>     if foo is B { // Optional<B>.some is added to foo's type stack
>         foo.someMethodSpecificToB()
>     } // Optional<B>.some is popped from foo's type stack
>     foo = nil // Type of foo is re-narrowed as Optional<A>.none
> } // Type of foo is Optional<A>.none in all branches
> foo.someMethod() // Error: foo is always nil at this point
>
> Here we can see that the extra condition (foo == bar) does not prevent
> type-narrowing, as the variable bar cannot be nil so both conditions
> require a type of Optional<A>.some as a minimum.
>
> In this example foo is also nil at the end of both branches, thus its
> type can remain narrowed past this point.
>
> <https://github.com/Haravikk/swift-evolution/tree/master/proposals#context-triggers>Context
> Triggers
> TriggerImpact
> as Explicitly narrows a type with as! failing and as? narrowing to Type? instead
> when this is not possible.
> is Anywhere a type is tested will allow the type-checker to infer the new
> type if there was a match (and other conditions agree).
> case Any form of exhaustive test on an enum type allows it to be narrowed
> either to that case or the opposite, e.g- foo != nil eliminates .none,
> leaving only .some as the type, which can then be implicitly unwrapped
> (see Enum Types above).
> = Assigning a value to a type will either narrow it if the new value is a
> sub-type, or will trigger widening to find a new common type, before
> attempting to re-narrow from there.
>
> There may be other triggers that should be considered.
>
> <https://github.com/Haravikk/swift-evolution/tree/master/proposals#impact-on-existing-code>Impact
> on existing code
>
> Although this change is technically additive, it will impact any code in
> which there are currently errors that type-narrowing would have detected;
> for example, attempting to manipulate a predictably nil value.
>
> <https://github.com/Haravikk/swift-evolution/tree/master/proposals#alternatives-considered>Alternatives
> considered
> One of the main advantages of type-narrowing is that it functions as an
> alternative to other features. This includes alternative syntax for
> shadowing/unwrapping of optionals, in which case type-narrowing allows an
> optional to be implicitly unwrapped simply by testing it, and without the
> need to introduce any new syntax.
>
> _______________________________________________
> 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/20161103/4801ebd7/attachment.html>


More information about the swift-evolution mailing list