[swift-evolution] [Proposal] Type Narrowing

Nevin Brackett-Rozinsky nevin.brackettrozinsky at gmail.com
Thu Nov 3 17:56:16 CDT 2016


Okay, I think I found an actual shortcoming that type narrowing might
address, namely mutating a property in place if it is a subtype. Here is a
small example setup:

protocol A { var x: Int {get} }
struct B: A { var x: Int }
struct C { var a: A }
var c = C(a: B(x: 4))

Note that “A” does not promise the “x” property will be mutable, while B
does. I use “x: Int” as a minimal example, but any solution should also
work for more complex scenarios.

Now suppose we wish to test whether “c.a” is of type B, and if so change
its “x” value. We could, of course, make a local copy, mutate it, and
reassign to “c.a”. But if those operations are expensive we would rather
avoid doing so. And if B uses copy-on-write, we would want to remove the
value from “c” entirely so that we hopefully have a unique reference. This
is hard to get right.

We would prefer to write something like the following:

(c.a as? B)?.x = 12

But that does not currently work, resulting in the error “Cannot assign to
immutable expression of type 'Int'”.

Will the proposed type-narrowing feature provide a simple way to mutate
“c.a” in place, contingent upon its being of type B?

How does it compare to an alternative such as inout return values, which
could preserve mutability in the above?

• • •

If we are going to have any sort of type narrowing, I would strongly prefer
that it be explicit. For example we could use a keyword such as “rebind” to
narrow the type of an existing symbol in a scope:

if rebind c.a as B {
    c.a.x = 12
}

Furthermore, I think the proposal to treat enum cases as types is a major
change to Swift’s type system, and probably introduces many unforeseen
headaches. It also smells somewhat of a backdoor way for union types to
sneak into the language.

Also, irrespective of everything else, you really need to make the
“Motivation” section of your proposal a lot stronger. That section should
stand on its own and make people understand exactly what problem you are
trying to solve and why it is important.

Nevin


On Thu, Nov 3, 2016 at 5:43 PM, Haravikk <swift-evolution at haravikk.me>
wrote:

>
> 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> 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 at swift.org
>> 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/ab8ca4eb/attachment.html>


More information about the swift-evolution mailing list