Swift Digest
SE-0508 | Swift Evolution

Array Expression Trailing Closures

Proposal
SE-0508
Authors
Cal Stephens
Review Manager
Xiaodi Wu
Status
Implemented (Swift Next)

01 何が問題だったのか

単一のクロージャを引数に取る init を、型名に続く trailing closure で呼び出すのは、Swift ではごく一般的な書き方です。たとえば result builder を使う次のような Array のイニシャライザは自然な拡張です。

extension Array {
    init(@ArrayBuilder build: () -> [Element]) {
        self = build()
    }
}

あるいは、クロージャが nil を返すまで要素を生成し続けるようなイニシャライザも考えられます。

extension Array {
    init(generate: () -> Element?) {
        self = []
        while let element = generate() {
            append(element)
        }
    }
}

ところが ArrayDictionary の場合、[String][String: Int] といった型表記に直接 trailing closure を付ける書き方はパーサーに受理されませんでした。{ ... } の部分は式の一部ではなく、周囲の宣言の一部(多くは “‘let’ declarations cannot be computed properties” などのエラー)か、あるいは別の未使用クロージャ(”closure expression is unused” エラー)として解釈されてしまいます。

// error: 'let' declarations cannot be computed properties
let value = [String] {
  "a"
}

// error: variable with getter/setter cannot have an initial value
var value = [String] {
  "a"
}

// error: closure expression is unused
let value = [String]
{
  "a"
}

これを避けるためには .init や空の括弧を挟む必要があり、他の型と比べて余計な手間がかかっていました。

let value = [String].init {
  "a"
}

let value = [String]() {
  "a"
}

一方で、同じ形の構文は InlineArray ではすでに通るようになっており、Array / Dictionary だけが例外的に書けないのは一貫性に欠ける状況でした。

let powersOfTwo = [4 of Int] { index in
  1 << index
}

背景には、式の中の [...] が常に「配列リテラル/辞書リテラル」として先にパースされるという事情があります。[String] も文法上はいったん要素が 1 つの配列リテラル(たとえば let String = "a" という前提で ["a"] を意味する可能性)として扱われ、型チェックの段階で必要に応じて型表記に読み替えられます。そして Swift のパーサーは、「リテラルの直後に来る { は trailing closure とみなさない」という規則を持っていたため、配列/辞書リテラルの後ろのブレースが trailing closure として拾われない、という構造的な制約になっていました。

02 どのように解決されるのか

パーサーの規則を緩め、配列リテラルおよび辞書リテラルの直後に書かれた { ... } を trailing closure として解釈するようにします。これにより、ArrayDictionary の型表記に直接 trailing closure を付ける書き方が、ほかの型と同じように通るようになります。

let value = [String] {
  "a"
}

let value = [String: Int] {
  (key: "a", value: 42)
}

これらはいずれも、対応する init(_:) を trailing closure で呼び出している形としてコンパイルされます。

callAsFunction との組み合わせ

同じ変更の副次的な効果として、配列リテラル値に対して callAsFunction を trailing closure で呼ぶこともできるようになります。

extension Array {
    func callAsFunction<T>(mapElement: (Element) -> T) -> [T] {
        map(mapElement)
    }
}

let value = ["a", "b", "c"] {
    $0.uppercased()
}

既存コードへの影響

この変更により、配列リテラルの後ろに独立して現れていたクロージャ式の意味が変わります。ただし、これまでコンパイルできていた書き方のうち大部分は、もともと “closure expression is unused” エラーで弾かれていたため、実際に動いていたコードへの影響はほぼありません。

唯一実コードで影響しうるのは、クロージャ値を受け付ける result builder の中で、配列リテラルの次の行に独立したクロージャリテラルを書いていたケースです。

@resultBuilder
enum FunctionArrayBuilder {
    static func buildBlock(_ components: (() -> Void)...) -> [() -> Void] {
        components
    }
}

@FunctionArrayBuilder
var buildFunctions: [() -> Void] {
    let array = ["a", "b", "c"]
    { print(array) }
}

この書き方は今回の変更後はコンパイルが通らなくなります({ print(array) }["a", "b", "c"] に対する trailing closure として吸収されます)。もっとも、このパターンはそもそも大変もろく、連続するクロージャリテラルをセミコロンなしで並べることができなかったり、間にローカル変数を一つ足すだけで成立しなくなったりと、実用に耐えるものではありませんでした。そのため提案では、このケースをとくに救済せず、素直にソース破壊として受け入れることが選ばれています。

今後の展望

今回の変更は配列リテラルと辞書リテラルに限定されていますが、将来的にはすべてのリテラルの直後に trailing closure を許すという拡張の方向性も議論されています(speculativeなもので、実現を約束するものではありません)。たとえば次のような文字列リテラルに対する callAsFunction の呼び出しは、現状では “closure expression is unused” として弾かれますが、同じ考え方を適用すれば自然に書けるようになります。

extension String {
  func callAsFunction(_ closure: (String) -> Void) {
    closure(self)
  }
}

"Hello world" {
  print($0)
}

ただし配列/辞書と違い、他のリテラル型には「型表記がリテラルとして先にパースされる」という特有の事情がないため、動機付けは今回ほど強くありません。