On Thu, Nov 3, 2016 at 14:23 Nevin Brackett-Rozinsky via swift-evolution < swift-evolution@swift.org> wrote:
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@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@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution