[swift-evolution] [Proposal] Type Narrowing

Haravikk swift-evolution at haravikk.me
Thu Nov 3 12:04:38 CDT 2016


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

Trigger	Impact
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.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20161103/f913385e/attachment.html>


More information about the swift-evolution mailing list