Swift Digest
SE-0049 | Swift Evolution

Move @noescape and @autoclosure to be type attributes

Proposal
SE-0049
Authors
Chris Lattner
Review Manager
Doug Gregor
Status
Implemented (Swift 3.0)

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 のマイグレータが自動で正確に書き換えます。利用者側では、マイグレータの適用後に手を入れる箇所はほとんどありません。