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