[Proposal] Type Narrowing


(Haravikk) #1

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.


Review of SE-0237: Introduce Contiguous Collection Protocols
(Callionica (Swift)) #2

Great. I'd like to see something like this. Couple of comments:

You don't explicitly address overload selection. Do you intend it to
be affected?

Impact on existing code section could be more descriptive.

···

On Thursday, November 3, 2016, 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.


#3

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


(Xiaodi Wu) #4

This proposal seems well-reasoned, but like Nevin I worry about the
interactions between type narrowing, reference types or capture of mutable
values, and concurrency or async. How will these things play together?

···

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


(Haravikk) #5

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 3 Nov 2016, at 19:23, Nevin Brackett-Rozinsky <nevin.brackettrozinsky@gmail.com> wrote:

On Thu, Nov 3, 2016 at 1:04 PM, Haravikk via swift-evolution <swift-evolution@swift.org <mailto: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

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@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution


(Chris Lattner) #6

FWIW, we have specifically considered something like this proposal in the past. You didn’t mention it, but the ?: operator is a specific example that frequently comes up. People often expect to be able to do something like:

  … = foo is B ? foo.bMethod() : …

which is the same sort of flow sensitive type refinement as you’re proposing here. This topic also came up in the design discussion around #available (which shipped in Swift 2, but was discussed much earlier), because unavailable decls could be available as optionals when not specifically checked for.

This is just MHO, but while I have been in favor of this in the (distant) past, I became convinced that this would be a bad idea when it comes to code maintenance over the long term. With our current (intentional shadowing based) design, you can always jump to the definition of a value to see where it was defined, and definitions always specify a type. Introducing flow senstitive type refinement breaks this model because the type of a decl depends not just on its declaration, but on a potentially arbitrary number of imperative checks that occur between the declaration and the use. This can make it much more difficult to understand code.

-Chris

···

On Nov 3, 2016, at 10:04 AM, 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.


(David Hart) #7

Very well said. I think this is perhaps the number one complaint I have about the proposal.

···

On 3 Nov 2016, at 20: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”.


#8

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@haravikk.me> wrote:

On 3 Nov 2016, at 19:23, Nevin Brackett-Rozinsky < > nevin.brackettrozinsky@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@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


(Haravikk) #9

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?

That's a good example, and yes it should be possible for type-narrowing to simplify stuff like this, I'll add a section on that I should probably go into more detail on how I intend working with narrowed mutable values to work, as for the advantage vs inout it's really just a matter of simplicity I think; using type narrowing should allow it to just work without having to pass to methods or design the API specifically for that kind of thing.

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
}

I don't see what's really gained by making it explicit vs implicit; if the condition was c.a is B then I'm not sure how that's any less clear?

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.

I'd say that in a sense enums are types, after all they can have unique associated value types of their own. Really though my intent is primarily to support optionals, but I figure it makes sense to try and do it from the perspective of general purpose enums since that's all optionals really are anyway.

Also, I've been thinking about thread safety and it occurs to me that type narrowing could actually be of significant benefit to it, rather than a detriment.

Consider:

  struct Foo { var value:Int }
  struct Bar { var foo?:Foo }

  var a = Foo(value: 5)
  var b = Bar(foo: a)

  b.foo.value = 10

Now, in this case we're only dealing with structs, and we know that b.foo has a value, thus b.foo.value is nice and safe; in fact no force unwrapping needs to occur behind the scenes, it can optimise away completely.

Instead, let's assume Bar is a class we're handling in a potentially shared way:

  class Bar { var foo?:Foo }

  func myMethod(bar:Bar) {
    b.foo = new Foo(5) // We now know that b.foo shouldn't be nil
    b.foo!.value = 10
  }

In this case, since Bar is a class we don't have total control over, we can't be certain that b.foo is non-nil when we try to modify it a second time; as a result, type-narrowing won't occur (because it's a class from another scope), however, because the type-checker knows that bar.foo should have a value, we can actually issue a more informative error message if force unwrapping fails, e.g- concurrent modification error.

In other words, we can potentially use it to help detect concurrency issues. It's another thing that's going to require a section on the proposal though, I have a feeling it's going to get pretty big!

···

On 3 Nov 2016, at 22:56, Nevin Brackett-Rozinsky <nevin.brackettrozinsky@gmail.com> wrote:


(Jean-Daniel) #10

For the case of the ?: operator, I think it can be replaced by a ?? operator for most cases:

… = (foo as B)?.bMethod() ?? …

···

Le 7 nov. 2016 à 04:52, Chris Lattner via swift-evolution <swift-evolution@swift.org> a écrit :

On Nov 3, 2016, at 10:04 AM, 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.

FWIW, we have specifically considered something like this proposal in the past. You didn’t mention it, but the ?: operator is a specific example that frequently comes up. People often expect to be able to do something like:

  … = foo is B ? foo.bMethod() : …


(Haravikk) #11

This seems like more of a challenge for the IDE; if it can tap into the type-checker then it can determine what the narrowed type is at any given point in your code, indeed I would expect it to for the purposes of auto-completion anyway. I know you don't necessarily want a language that's reliant on good IDE support, but if you're doing something complex enough where this would become a problem and NOT using a good IDE then it seems kind of like a self-inflicted problem to me.

Even so there's nothing in this feature that would prevent you from using shadowing if you want to, for example if a block is especially large and you feel it adds clarity.

Actually though I'd say that for maintenance narrowing may be better, as it can clarify what a type is supposed to be at a given point, and if you break the narrowing then you'll create errors and warning that show you how you've changed the meaning of the code. Consider for example:

  func doSomething(value:Int?) {
    if (value == nil) { value = 5 } // value is narrow to Optional<Int>.some

    // Lots of really important code that never causes value to become nil

    print(value!.description)
  }

Say you come back later and decide to remove the conditional at the top, now that value!, though a fair assumption at the time, can cause a runtime failure. With narrowing however you wouldn't have had to force unwrap because of the known non-nil value, but your change will break that, resulting in an error that forces you to fix it.

I'm still struggling how best to phrase my motivation section; so far I seem to have an increasingly large grab-bag of individual problems that type-narrowing can solve, with no way to put it more succinctly.

···

On 7 Nov 2016, at 03:52, Chris Lattner <clattner@apple.com> wrote:
Introducing flow senstitive type refinement breaks this model because the type of a decl depends not just on its declaration, but on a potentially arbitrary number of imperative checks that occur between the declaration and the use. This can make it much more difficult to understand code.


#12

The more I think about it, the more I realize what I actually want is an
“alias” or “view” or “lens” that will let me give a new name to an existing
entity, without copying or moving it.

This is useful in general if you need to work “down the rabbit hole”, so
you could write something like:

alias size = anArray[idx].someProperty.size
// Work with “size” as a shorthand, including mutations

In the situation from my previous example, where we only want to take
action when the nested property is a certain type, it might look like:

if alias b = c.a as? B {
    b.x = 12
}

I think this sort of feature is far more versatile than type-narrowing, and
it retains the clarity of type information in source code.

Nevin

···

On Sun, Nov 6, 2016 at 10:52 PM, Chris Lattner via swift-evolution < swift-evolution@swift.org> wrote:

> On Nov 3, 2016, at 10:04 AM, 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.

FWIW, we have specifically considered something like this proposal in the
past. You didn’t mention it, but the ?: operator is a specific example
that frequently comes up. People often expect to be able to do something
like:

        … = foo is B ? foo.bMethod() : …

which is the same sort of flow sensitive type refinement as you’re
proposing here. This topic also came up in the design discussion around
#available (which shipped in Swift 2, but was discussed much earlier),
because unavailable decls could be available as optionals when not
specifically checked for.

This is just MHO, but while I have been in favor of this in the (distant)
past, I became convinced that this would be a bad idea when it comes to
code maintenance over the long term. With our current (intentional
shadowing based) design, you can always jump to the definition of a value
to see where it was defined, and definitions always specify a type.
Introducing flow senstitive type refinement breaks this model because the
type of a decl depends not just on its declaration, but on a potentially
arbitrary number of imperative checks that occur between the declaration
and the use. This can make it much more difficult to understand code.

-Chris
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Rien) #13

Exactly.
-1.

Regards,
Rien

Site: http://balancingrock.nl
Blog: http://swiftrien.blogspot.com
Github: http://github.com/Swiftrien
Project: http://swiftfire.nl

···

On 09 Nov 2016, at 07:51, David Hart via swift-evolution <swift-evolution@swift.org> wrote:

On 3 Nov 2016, at 20: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”.

Very well said. I think this is perhaps the number one complaint I have about the proposal.


(Charlie Monroe) #14

I'd personally not make this automatic, but require explicit action from the developer.

In case of nullability, I have previously suggested "nonnil" keyword:

let foo: String? = "Hello World"
guard nonnil foo else {
    return
}

In which way you explicitly request the type narrowing. Or:

let foo: Any
guard foo as String else {
    return
}

I.e. not using "is" which returns a boolean, but using the cast operator, which IMHO makes more sense and prevents from unintentional type narrowing...

···

On Nov 7, 2016, at 12:34 PM, Haravikk via swift-evolution <swift-evolution@swift.org> wrote:

On 7 Nov 2016, at 03:52, Chris Lattner <clattner@apple.com <mailto:clattner@apple.com>> wrote:
Introducing flow senstitive type refinement breaks this model because the type of a decl depends not just on its declaration, but on a potentially arbitrary number of imperative checks that occur between the declaration and the use. This can make it much more difficult to understand code.

This seems like more of a challenge for the IDE; if it can tap into the type-checker then it can determine what the narrowed type is at any given point in your code, indeed I would expect it to for the purposes of auto-completion anyway. I know you don't necessarily want a language that's reliant on good IDE support, but if you're doing something complex enough where this would become a problem and NOT using a good IDE then it seems kind of like a self-inflicted problem to me.

Even so there's nothing in this feature that would prevent you from using shadowing if you want to, for example if a block is especially large and you feel it adds clarity.

Actually though I'd say that for maintenance narrowing may be better, as it can clarify what a type is supposed to be at a given point, and if you break the narrowing then you'll create errors and warning that show you how you've changed the meaning of the code. Consider for example:

  func doSomething(value:Int?) {
    if (value == nil) { value = 5 } // value is narrow to Optional<Int>.some

    // Lots of really important code that never causes value to become nil

    print(value!.description)
  }

Say you come back later and decide to remove the conditional at the top, now that value!, though a fair assumption at the time, can cause a runtime failure. With narrowing however you wouldn't have had to force unwrap because of the known non-nil value, but your change will break that, resulting in an error that forces you to fix it.

I'm still struggling how best to phrase my motivation section; so far I seem to have an increasingly large grab-bag of individual problems that type-narrowing can solve, with no way to put it more succinctly.
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Haravikk) #15

Normally I'm a proponent of being more rather than less explicit, but the biggest draw of type-narrowing to me is that you're *already* telling the type-checker, it's really just confirming what you know automatically.

So if I do:

  if foo is String {
    // Do lots of non-string stuff
    (foo as String).somethingStringSpecific
  }

On the last line of the block the type-checker is able to remind me that I already know that foo is a String, so I don't need to cast it.

Really when it comes down to it the type-narrowing never takes anything away from you; you can always handle the value as a less specific (wider) type if you want to, but if you want to treat it like a String because you know it's one, then you can do that too.

I'm concerned that if the feature had to be explicit, it would lack discoverability, and really the point is almost to get rid of the need to do things explicitly when you don't need to. It's like type inference on overdrive in a way.

I guess I just don't see why you'd think that "guard nonnil foo" is really more explicit than "guard foo != nil", in both cases you know that foo can't be nil past that point, so is a new keyword really justified?

···

On 7 Nov 2016, at 11:58, Charlie Monroe <charlie@charliemonroe.net> wrote:

I'd personally not make this automatic, but require explicit action from the developer.

In case of nullability, I have previously suggested "nonnil" keyword:

let foo: String? = "Hello World"
guard nonnil foo else {
    return
}

In which way you explicitly request the type narrowing. Or:

let foo: Any
guard foo as String else {
    return
}

I.e. not using "is" which returns a boolean, but using the cast operator, which IMHO makes more sense and prevents from unintentional type narrowing…


(Charlie Monroe) #16

I'm simply worried a little about unwanted effects and additional compiler "cleverness". I'd simply much rather opt-in to it using a keyword or a slightly different syntax. And instead of

  if foo is String { ... }

I'd prefer

  if foo as? String { ... }

which is syntactically closer to

  if let foo = foo as? String { ... }

which is generally what we're after.

Also, it would maintain code compatibility. The current proposal would change semantics of the code - mostly when comparing to nil.

Xcode's migration is "nice", but I'd like to point out that migration to Swift 3 of my project took 6 hours (!) and I spent almost 2 more days manually changing what the migrator didn't manage to do on its own. And that was one of my projects. I really don't want to go through this once more.

Not to mention the already growing non-macOS base of Swift users.

I know now is the time for the last incompatible changes, but are the benefits of implicit type narrowing so great to warrant this?

···

On Nov 7, 2016, at 2:08 PM, Haravikk <swift-evolution@haravikk.me> wrote:

On 7 Nov 2016, at 11:58, Charlie Monroe <charlie@charliemonroe.net <mailto:charlie@charliemonroe.net>> wrote:

I'd personally not make this automatic, but require explicit action from the developer.

In case of nullability, I have previously suggested "nonnil" keyword:

let foo: String? = "Hello World"
guard nonnil foo else {
    return
}

In which way you explicitly request the type narrowing. Or:

let foo: Any
guard foo as String else {
    return
}

I.e. not using "is" which returns a boolean, but using the cast operator, which IMHO makes more sense and prevents from unintentional type narrowing…

Normally I'm a proponent of being more rather than less explicit, but the biggest draw of type-narrowing to me is that you're *already* telling the type-checker, it's really just confirming what you know automatically.

So if I do:

  if foo is String {
    // Do lots of non-string stuff
    (foo as String).somethingStringSpecific
  }

On the last line of the block the type-checker is able to remind me that I already know that foo is a String, so I don't need to cast it.

Really when it comes down to it the type-narrowing never takes anything away from you; you can always handle the value as a less specific (wider) type if you want to, but if you want to treat it like a String because you know it's one, then you can do that too.

I'm concerned that if the feature had to be explicit, it would lack discoverability, and really the point is almost to get rid of the need to do things explicitly when you don't need to. It's like type inference on overdrive in a way.

I guess I just don't see why you'd think that "guard nonnil foo" is really more explicit than "guard foo != nil", in both cases you know that foo can't be nil past that point, so is a new keyword really justified?


(Haravikk) #17

I'm simply worried a little about unwanted effects and additional compiler "cleverness".

I don't believe there should be any; either the type is narrowed or it isn't, if you rely on it being a type the type-checker can't verify, you'll get an error, otherwise you won't. There shouldn't be scope for anything unexpected.

Xcode's migration is "nice", but I'd like to point out that migration to Swift 3 of my project took 6 hours (!) and I spent almost 2 more days manually changing what the migrator didn't manage to do on its own. And that was one of my projects. I really don't want to go through this once more.

I agree, but the only code that should be affected by this is code where there is unwrapping that can be determined to either be redundant, or is definitely incorrect; in the former case it will only be a warning (so you can remove force unwrapping that is no longer needed) and in the latter it will be an error because the type-checker has actually identified something that will definitely cause a run-time error.

···

On 7 Nov 2016, at 16:29, Charlie Monroe <charlie@charliemonroe.net> wrote:


(TJ Usiyan) #18

I am mostly opposed because I don't see how this could avoid being
complicated to explain compiler magic. Making this accessible as a feature
for our types and operations would be a challenge and doesn't look to have
a worthwhile yield for the effort. We can accomplish most, if not all, of
this with shadowing.

TJ

···

On Mon, Nov 7, 2016 at 2:03 PM, Haravikk via swift-evolution < swift-evolution@swift.org> wrote:

> On 7 Nov 2016, at 16:29, Charlie Monroe <charlie@charliemonroe.net> > wrote:
> I'm simply worried a little about unwanted effects and additional
compiler "cleverness".

I don't believe there should be any; either the type is narrowed or it
isn't, if you rely on it being a type the type-checker can't verify, you'll
get an error, otherwise you won't. There shouldn't be scope for anything
unexpected.

> Xcode's migration is "nice", but I'd like to point out that migration to
Swift 3 of my project took 6 hours (!) and I spent almost 2 more days
manually changing what the migrator didn't manage to do on its own. And
that was one of my projects. I really don't want to go through this once
more.

I agree, but the only code that should be affected by this is code where
there is unwrapping that can be determined to either be redundant, or is
definitely incorrect; in the former case it will only be a warning (so you
can remove force unwrapping that is no longer needed) and in the latter it
will be an error because the type-checker has actually identified something
that will definitely cause a run-time error.
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Charlie Monroe) #19

I'm simply worried a little about unwanted effects and additional compiler "cleverness".

I don't believe there should be any; either the type is narrowed or it isn't, if you rely on it being a type the type-checker can't verify, you'll get an error, otherwise you won't. There shouldn't be scope for anything unexpected.

True.

I'm simply worried about the compiler speed in general, that's what I meant by "cleverness". The implicit variable typing and other features of Swift already IMHO make it incredibly slow and various features analyzing the code in order to determine the correct type can slow it even further. For comparison, a 100KLOC project of mine in pure Swift takes about 8 minutes to compile, vs. 3 minutes when it was in ObjC. With no optimizations turned on.

But I agree that designing a language around the compiler speed is wrong, but I believe designing the language without taking it into account is just as wrong. It's not worth designing features that would make the compilation so slow it would render the language unusable.

Note that I have only very limited experience with compiler implementation, I've only made a few minor things with Clang a few years back, so please feel free to correct me.

Xcode's migration is "nice", but I'd like to point out that migration to Swift 3 of my project took 6 hours (!) and I spent almost 2 more days manually changing what the migrator didn't manage to do on its own. And that was one of my projects. I really don't want to go through this once more.

I agree, but the only code that should be affected by this is code where there is unwrapping that can be determined to either be redundant, or is definitely incorrect; in the former case it will only be a warning (so you can remove force unwrapping that is no longer needed) and in the latter it will be an error because the type-checker has actually identified something that will definitely cause a run-time error.

There are two cases:

if foo != nil {
    foo!.doSomething()
}

Currently, accessing a non-optional value with ! produces an error:

let foo = Bar()
foo!.doSomething() // ERROR

Second:

if foo != nil {
    // Using ? to be extra cautious, if foo is var
    foo?.doSomething()
}

This again currently produces an error:

let foo = Bar()
foo?.doSomething() // ERROR

Which is generally, what would semantically happen - the variable would loose it optionality. Or am I wrong?

Which would require migration, where within all scopes, where you check if a a variable is not nil, it removes all ? and !...

···

On Nov 7, 2016, at 8:03 PM, Haravikk <swift-evolution@haravikk.me> wrote:

On 7 Nov 2016, at 16:29, Charlie Monroe <charlie@charliemonroe.net> wrote:


(Haravikk) #20

There are two cases:

if foo != nil {
    foo!.doSomething()
}

Currently, accessing a non-optional value with ! produces an error:

let foo = Bar()
foo!.doSomething() // ERROR

Second:

if foo != nil {
    // Using ? to be extra cautious, if foo is var
    foo?.doSomething()
}

This again currently produces an error:

let foo = Bar()
foo?.doSomething() // ERROR

Which is generally, what would semantically happen - the variable would loose it optionality. Or am I wrong?

I probably haven't clarified well enough but under type-narrowing these would be warnings rather than errors; i.e- the general type of foo is still Optional, the type-checker merely knows that it can't be nil at that point, so would inform you that the ? or ! are unnecessary.

This is what I was trying to get at in the type-widening section; basically, if you have a variable whose type is narrowed, but do something that makes no sense for the narrowed type, then the type is widened until either a match is found or it can't go any wider (producing an error as normal).

So in your examples foo is Optional<Bar>.some, as a result the ? and ! operators make no sense, so the type is widened back to Optional<Bar> where it does make sense and the code compiles, but a warning is produced to inform you that you don't need to use those operators.

···

On 7 Nov 2016, at 19:31, Charlie Monroe <charlie@charliemonroe.net> wrote:

On 7 Nov 2016, at 19:31, Charlie Monroe <charlie@charliemonroe.net> wrote:
I agree that designing a language around the compiler speed is wrong, but I believe designing the language without taking it into account is just as wrong. It's not worth designing features that would make the compilation so slow it would render the language unusable.

Note that I have only very limited experience with compiler implementation, I've only made a few minor things with Clang a few years back, so please feel free to correct me.

I'm not that familiar with the actual architecture either, but narrowing *should* be fairly simple; basically any time the compiler hits a condition or statement defined as a narrowing trigger, it pops the new narrower type onto a stack of types for that variable (in that branch). Now whenever the compiler reaches another statement for that variable (method call etc.) it resolves it first against the narrowest type, otherwise it goes up the stack (widening) till it finds a match or fails.

When a branch closes with a stack of types, the compiler will compare to other branches to see which type is the narrowest that they have in common; this is actually fairly simple (shorten the stack for each branch to the length of the shortest stack, then discard elements until the current one is a match for all branches, thus you now know what the narrowest type is past that point).

So, for types that never narrow there should be no speed difference, while for narrowed types there shouldn't be much of a difference, as these stacks of types shouldn't get very large in most cases (I'd expect anything more than three to be pretty rare).