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.
_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to