Move @noescape and @autoclosure to be type attributes
01 何が問題だったのか
Swift 2 までの @noescape と @autoclosure は、パラメータ宣言に対する属性(declaration attribute)として扱われていました。つまり、次のようにコロンの左側、パラメータ名の前に書くスタイルです。
func f(@noescape fn: () -> ()) {}
func f2(@autoclosure a: () -> ()) {}
これらの属性は意味的には「渡されたクロージャの型」に関する性質(エスケープするかどうか、呼び出し側の式を自動でクロージャに包むかどうか)を表していますが、構文上は引数宣言側に張り付いているというずれがありました。このずれが、いくつかの具体的な不便さを生んでいました。
関数型として書けない
SE-0002 でカリー化された関数宣言構文が取り除かれた影響で、カリー化された形の関数を手で書くコードが使えなくなるケースが出てきました。たとえば、次のような flatMap のラッパーを手書きで書こうとしたとします。
func curriedFlatMap<A, B>(x: [A]) -> (@noescape A -> [B]) -> [B] {
return { f in
x.flatMap(f)
}
}
この書き方は、@noescape がパラメータ宣言にしか付けられなかったため、「関数型の中に @noescape が出てくる位置」で拒否されていました。この問題自体は後に @noescape を任意の関数型にも書けるようにすることで解消されましたが、結果として「宣言にも書けるし型にも書ける」という 冗長な二重表記 が残ってしまっていました。
func f(@noescape fn: () -> ()) {} // declaration attribute
func f(fn: @noescape () -> ()) {} // type attribute
// 同じ意味の書き方が2種類ある
@autoclosure では型をまったく書けない
@autoclosure は @noescape よりさらに悪い状況にありました。@autoclosure を含む関数型を、そもそも 明示的な型注記として書き下すことができない のです。
たとえば次のように、@autoclosure を持つ関数を定義して、それを別の変数に代入すること自体は可能です。
func f2(@autoclosure a: () -> ()) {}
let x = f2
x(print("hello"))
このとき x の型は (@autoclosure () -> ()) -> () になっており、わざと型エラーを起こせばコンパイラがその型を表示してくれます。
let y: Int = x
// error: cannot convert value of type '(@autoclosure () -> ()) -> ()' to specified type 'Int'
ところが、同じ型を自分で書こうとすると拒否されてしまいます。
let x2: (@autoclosure () -> ()) -> () = f2
// error: attribute can only be applied to declarations, not types
型推論では扱える型を、手書きの型注記では書けない、という一貫性のない状況でした。
SE-0031 との足並み
同じ時期に行われた SE-0031 では、inout をパラメータ宣言側から型側の修飾子へと移すことで、宣言構文と型構文の位置を揃える整理が進められていました。@noescape と @autoclosure についても同じ方向で整理しないと、宣言と型のあいだの位置関係がキーワードごとにバラバラになってしまいます。
02 どのように解決されるのか
@noescape と @autoclosure を パラメータ宣言に書くことをやめ、関数型の側に書く型属性(type attribute)として扱う ように変更します。書く位置は、パラメータ名とコロンの後ろ、関数型の直前です。
// Before (Swift 2):
func f(@noescape fn: () -> ()) {}
func f2(@autoclosure a: () -> ()) {}
// After (Swift 3):
func f(fn: @noescape () -> ()) {}
func f2(a: @autoclosure () -> ()) {}
これにより、宣言の中での書き方と、関数そのものの型としての書き方が一致します。たとえば上の f の型は (_: @noescape () -> ()) -> ()、f2 の型は (_: @autoclosure () -> ()) -> () で、宣言側に書いた @noescape / @autoclosure の位置と型表記での位置が揃います。
@autoclosure を含む型が書けるようになる
@autoclosure を型属性として認めたことで、これまで明示的な型注記として書けなかった @autoclosure 付きの関数型も、そのまま書けるようになります。
func f2(a: @autoclosure () -> ()) {}
// Swift 2 では書けなかったが、Swift 3 では書ける
let x2: (@autoclosure () -> ()) -> () = f2
x2(print("hello"))
型推論で得られる型と、手書きできる型の表記が一致するようになります。
冗長な二重表記がなくなる
@noescape についても、宣言側に書く形はなくなり、型側に書く形が唯一のスタイル になります。同じ意味を2通りに書ける状態は解消され、スタイル選択の迷いがなくなります。
// Swift 3 ではこれだけが正しい書き方
func run(fn: @noescape () -> ()) { fn() }
既存コードへの影響と移行
この変更は、既存の @noescape / @autoclosure の書き方に対しては互換性を壊す変更です。Swift 3 では古い位置への記述は エラー になります。ただし書き換えは純粋に構文上の位置の移動なので、Swift 3 のマイグレータが自動で正確に書き換えます。利用者側では、マイグレータの適用後に手を入れる箇所はほとんどありません。