[swift-evolution] [Draft] Change @noreturn to unconstructible return type

Brent Royal-Gordon brent at architechies.com
Wed Jun 8 09:09:51 CDT 2016


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



More information about the swift-evolution mailing list