[swift-evolution] [Proposal] Type Narrowing

Haravikk swift-evolution at haravikk.me
Thu Nov 3 16:43:16 CDT 2016


> On 3 Nov 2016, at 19:23, Nevin Brackett-Rozinsky <nevin.brackettrozinsky at gmail.com> wrote:
> For the motivating examples, there is already a much cleaner solution:
> (foo as? B)?.someMethodSpecificToB()

Eh, kinda; you've taken an example showing branching and boiled it down into a single line; what if my intention was to call multiple B specific methods on foo? I think you've seen a simple example and tried to simplify it further, but I suppose I could put another method call in it to clarify.

> Aside from any implementation concerns, this proposal substantially increases the cognitive load on developers.

I'm not so sure of this; the type is never going to be wider than what you originally set, so at the very least you can do whatever its original explicit type or inferred type would let you do, the main difference is that if you do something that makes no sense anymore then the type-checker can inform you of this. Otherwise if you know you've narrowed the type, then you can do things with that narrowed type and either the type-checker will allow it, or inform you that it's not possible, thus warning you that your conditional(s) etc. don't work as intended.

Really the burden is no different to the techniques that already exist; if you've tested a value with `is` then you know what the type is within that block of code, this is just ensuring that the type checker also knows it. Likewise with an optional, if you test that it's not nil then you know it's not, and so should the type-checker.

I should probably put more under motivation about why this feature works for the example cases given, and what the impact is on a larger scale; as the narrowing has the potential to pick up a bunch of little errors that standard type-checking alone may not, the trick is coming up with the example code to show it that isn't enormous, as I'm trying to avoid too much complexity in the Motivation section so I can build up the examples; maybe I'll add an "Advantages" section further down to detail more of what can be done *after* demonstrating the feature in stages, rather than trying to do it at the start.

> 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!
> }

Actually the issue is visible in the code; it's the conditional. If you can't trust `foo` not to change then testing its value and then acting upon the result without some form of local copy or locking is the part that explicitly isn't thread safe.

I'm not really sure of the need to focus on thread safety issues here as I'm not sure they're unique to this feature, or even to optionals; anything that is part of a shared class and thus potentially shared with other threads is unsafe, whether it's optional or not. While optionals might produce errors if force unwrapped and nil, they just as easily might not but end up with inconsistent values instead, so I'm not sure having the force unwrap operators actually makes you any safer; put another way, if you're relying on force unwrapping to catch thread safety issues then I'm not sure that's a good strategy, as it can only detect the issues at runtime, and only if the exact conditions necessary actually occur to trigger the failure.

I don't know what's planned for Swift's native concurrency support, but personally I'm hoping we might get a feature of classes that requires them (and/or their methods) to be marked as explicitly safe (except where it can be inferred), producing warnings otherwise, forcing developers to consider if their type/method has been properly reviewed for thread safety. Thread safety after all really is specific to classes, which are mostly discouraged in Swift anyway, so it makes sense to focus efforts towards making classes safer, and that you're handling your struct copies efficiently.

>> On Thu, Nov 3, 2016 at 1:04 PM, Haravikk via swift-evolution <swift-evolution at swift.org <mailto: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
>> 
>> 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
>> swift-evolution at swift.org <mailto:swift-evolution at swift.org>
>> https://lists.swift.org/mailman/listinfo/swift-evolution <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/85c3cc78/attachment.html>


More information about the swift-evolution mailing list