Remove @noreturn attribute and introduce an empty Never type
01 何が問題だったのか
exit や fatalError、dispatchMain のように、呼び出したら決して戻ってこない関数があります。こうした関数を型システムに伝えるため、Swiftには @noreturn という関数型の属性が用意されていました。この属性を付けておくと、関数呼び出しの後ろに return が書かれていなかったり、guard ... else から抜け出していなかったりしても、コンパイラが制御フローに関する誤った診断を出さずに済みます。
@noreturn は関数型の直交する一軸として扱いづらい
@noreturn は関数型に付く属性なので、throws や戻り値の型といった他の要素との組み合わせを、ひとつずつ仕様として定義しなければなりませんでした。たとえば次のような疑問がいちいち発生します。
@noreturn throwsは「正常には戻らないが throw はできる」という意味か、それとも「一切戻らない(throw もしない)」という意味か@noreturn () -> Intは許されるのか、許されるとして@noreturn () -> ()とどう違うのか- 関数合成のようなジェネリックな処理が
@noreturnでパラメータ化されるべきか(compose(exit, getExitCode)は@noreturnになるべきか)
属性として独立しているぶん、言語機能と掛け合わされるたびに意味を決めていく必要があり、モデル全体が不必要に複雑になっていました。
実は「値を持たない型」で表現できる
一方で、Swiftにはすでに uninhabited type(値を持たない型) を定義する手段があります。ケースを1つも持たない enum は、正当な値を1つも持たず、インスタンスを作ることもできません。Swiftユーザーは、こうした空の enum を名前空間として使うテクニックをすでに活用していました。
戻り値の型が uninhabited type である関数は、値を返しようがないので、正常に戻ることがありえない ことが型から直接読み取れます。つまり @noreturn という属性を新たに導入せずとも、「戻らない」という性質は既存の型システムだけで表現できるのです。
/// The type of expressions that can never happen.
public /*closed*/ enum Never { /*no values*/ }
func foo() -> Never {
fatalError("no way out!")
}
この見方に立てば、先ほどの @noreturn にまつわる疑問はすべて自然に解消されます。() throws -> Never は「正常には戻らないが throw はできる」関数を素直に表現できますし、「戻り値の型を持ちつつ戻らない関数」という矛盾した状態を宣言することは不可能になります。また、Never はファーストクラスの型なので、特別な言語機能を追加しなくても、ジェネリックな処理を通じて自然に伝播していきます。
@noreturn 属性は、型システムで素直に表せるはずのものを、わざわざ別軸の属性として持ち込んでいる、というのが出発点の問題意識です。
02 どのように解決されるのか
@noreturn 属性を言語から削除し、「戻らない関数」は uninhabited type を戻り値の型として宣言する ことで表現するようにします。制御フロー診断(guard ... else からの脱出や、非 Void 関数での return 忘れなど)の免除は、uninhabited type の式に対して与えられるようになります。
標準ライブラリに Never 型を追加する
標準ライブラリにケースを持たない public な closed enum Never を追加し、「戻らない」ことを表す戻り値の型として慣例的に使います。
public /*closed*/ enum Never {
/* this space intentionally left blank */
}
fatalError などの既存の @noreturn 関数はすべて -> Never に変更されます。Clangインポータも、__attribute__((noreturn)) の付いたC / Objective-C関数を、Swiftからは Never を返す関数としてインポートするようになります。
uninhabited type の判定ルール
コンパイラは、次のルールで型が uninhabited かどうかを判定します。
enumは、ケースが1つも存在しないか、あるいは全ケースが既知でそのすべてが associated value を持ち、かつそれらの associated value の型がすべて空であれば、uninhabited とみなされます。ただしresilienceモデルの下では、外部モジュールのpublicenum はclosedでない限り、あとから非公開のケースが追加されているかもしれないため、空とは仮定できません。- タプル・構造体・クラスは、uninhabited type の stored property を1つでも持てば uninhabited とみなされます。こちらもresilienceの都合上、信頼できるのは外部モジュールの fragile な型に限られます。
- 関数型とメタタイプは、uninhabited にはなりません。
uninhabited type の式は「到達不能」として扱われる
uninhabited type の式を評価する箇所は、制御フロー診断において「そこから先へは進まない」と扱われます。たとえば戻り値が Never の関数を呼び出せば、その後ろに return や guard からの脱出を書かなくてもコンパイラは文句を言いません。
func noReturn() -> Never {
fatalError() // fatalError also returns Never, so no need to `return`
}
func pickPositiveNumber(below limit: Int) -> Int {
guard limit >= 1 else {
noReturn()
// No need to exit guarded scope after noReturn
}
return rand(limit)
}
uninhabited type を返す式を値として使わず捨てても、「結果が使われていない」という警告は出ません。逆に、uninhabited type の式の後ろに書かれたコードは「実行されない」という警告の対象となります。
Never という名前
標準ライブラリの uninhabited type の名前は慎重に選ばれました。Void は数学的には妥当ですが、C系言語では「unit」と混同されがちです。Nothing や Nil は Optional の nil と紛らわしく、Bottom のような型理論の用語はユーザーにとって直観的とは言えません。最終的に選ばれた Never は、「この関数からは 決して 戻らない」という時間的なニュアンスをうまく表します。また、将来 typed throws のような機能が入った場合でも、() throws<Never> -> Void のように「決して throw しない関数」として自然に使い回せる余地があります。
「bottom type」としての扱いについて
uninhabited type は、理論上はあらゆる型の部分型(bottom type)とみなすこともでき、たとえば array.filter(fatalError) のように戻らない関数を高階関数に直接渡せるようにする余地があります。ただしこの拡張は別途提案するものとし、本提案のスコープには含めません。@noreturn から -> Never への移行に際しても、これまで @noreturn (T...) -> U を (T...) -> V に変換することは許されていなかったため、表現力の後退はありません。Void コンテキストで戻らない関数を使う、という最も重要なユースケースは、既存の (T...) -> U から (T...) -> Void への部分型関係によって引き続き機能します。
既存コードへの移行
実際に使われている @noreturn 関数は数が少なく、そのほとんどは Void を返しているため、-> Never への置き換えは機械的に行えます。自前で @noreturn 関数を書いている場合は、戻り値の型を Never に変更するだけで済みます。