>> The problem is, types flow through the type system. Use a NoReturn method 
>> with optional chaining, now you have an Optional<NoReturn>. flatMap over 
>> that Optional<NoReturn>, now you have a parameter of type NoReturn. What's a 
>> *parameter* of type NoReturn? You'd want it to be, say, a different bottom 
>> type named NoPass, but you can't—the type came from a NoReturn, and it's 
>> stuck being a NoReturn.
> 
> This is a method that *does not return*. The compiler should error if you try 
> to use the “result” of a no return function. In fact, it should error if 
> there is any more code after the method that can’t be reached by a path that 
> avoids the call.

Like the path introduced by optional chaining, which I *specifically* mentioned 
in the paragraph you quoted so scathingly.

Let me write out an example explicitly so you can see what I mean. Suppose you 
have this protocol:

        protocol Runnable {
                associatedtype Result
                func run() -> Result
                static func isSuccessful(_ result: Result) -> Bool
        }

And you implement this generic function to use it:

        func runAndCheck<R: Runnable>(_ runner: R?) -> Bool {
                return runner?.run().flatMap(R.isSuccessful) ?? true
        }

With intermediate variables and explicit type annotations, that's:

        func runAndCheck<R: Runnable>(_ runner: R?) -> Bool {
                let optionalResult: R.Result? = runner?.run()
                let optionalSuccess: Bool? = 
processedResult.flatMap(R.isSuccessful)
                let success: Bool = optionalSuccess ?? true
                return success
        }

Now, suppose `R` is a `RunLoop` type with a `run()` method that never returns. 
(`NSRunLoop` is similar to this type, although its `run()` actually can return; 
you just can't really count on it ever doing so.) Then you have this:

        class RunLoop: Runnable {
                …
                associatedtype Result = Never
                
                func run() -> Never {
                        …
                }
                
                class func isSuccessful(_ result: Never) -> Bool {
                        // Uncallable due to Never parameter
                }
        }

And `runAndCheck(_:)` specializes to:

        func runAndCheck(_ runner: RunLoop?) -> Bool {
                let optionalResult: Never? = runner?.run()
                let optionalSuccess: Bool? = 
processedResult.flatMap(RunLoop.isSuccessful)
                let success: Bool = optionalSuccess ?? true
                return success
        }

So, as you can see, this code *does* have a path beyond the non-returning call: 
the path where `runner` is `nil`.

This code benefits in several ways from using a bottom type to express 
`run()`'s non-returning nature:

1. The protocol now correctly expresses the fact that, if `run()` can never 
return, then `isSuccessful(_:)` can never be called. With a `@noreturn` 
attribute, `run()` would have to have some kind of fake return type (probably 
`Void`), and `isSuccessful` would be callable with a `Void`.

2. Because the compiler can prove that `isSuccessful(_:)` can never be called, 
there's no need to provide any implementation for it. The compiler can prove 
that any implementation you might provide is dead code, because it could only 
be reached after code that would have to instantiate a `Never`. With a 
`@noreturn` attribute, `Result` would be some fake type like `Void`, and the 
compiler could not prove that `isSuccessful(_:)` is unreachable.

3. Since `optionalResult` is of type `Never?`, the compiler can prove that it 
cannot ever be `.some`. To construct a `.some`, you would need to instantiate a 
`Never`, which is impossible. With a `@noreturn`-type solution, 
`optionalResult` would be of (say) type `Void?`, and the fact that `.some` was 
impossible would be lost to us.

This means that:

1. If the compiler inlines `Optional<Never>.flatMap`, it can eliminate the 
`.some` case (since it can never match), then eliminate the `switch` statement 
entirely, turning the method into simply `return .none`.

        func runAndCheck(_ runner: RunLoop?) -> Bool {
                let optionalResult: Never? = runner?.run()
                let optionalSuccess: Bool? = .none
                let success: Bool = optionalSuccess ?? true
                return success
        }

2. The compiler can then notice that `optionalSuccess` is always `.none`, so 
`success` is always `true`.

        func runAndCheck(_ runner: RunLoop?) -> Bool {
                let optionalResult: Never? = runner?.run()
                let success: Bool = true
                return success
        }

3. The compiler can then notice that `optionalResult` is never actually used, 
and eliminate that value.

        func runAndCheck(_ runner: RunLoop?) -> Bool {
                _ = runner?.run()
                let success: Bool = true
                return success
        }

Could the compiler have done this without using `Never`? Maybe; the information 
is still there if you look (at least if it knows `run()` is @noreturn). But 
it's less straightforward to make it flow through the entire function like 
this. If we use a bottom type to represent the fact that a function cannot 
return, then the type system naturally carries this information to all the 
places where it's needed.

-- 
Brent Royal-Gordon
Architechies

_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to