Multiple Trailing Closures
01 何が問題だったのか
Swift には初期から trailing closure syntax(末尾クロージャ構文)がありました。関数の最後の引数がクロージャである場合、その引数を括弧の外に「押し出す」ことができる糖衣構文です。たとえば UIView.animate(withDuration:animations:) は次のように書けます。
// trailing closure を使わない書き方
UIView.animate(withDuration: 0.3, animations: {
self.view.alpha = 0
})
// trailing closure を使った書き方
UIView.animate(withDuration: 0.3) {
self.view.alpha = 0
}
trailing closure は呼び出し側を簡潔かつネストの浅いかたちにでき、Swift の API と相性がよいことから広く使われてきました。しかし 最後のクロージャ 1 つにしか適用できない という制約のため、クロージャを複数受け取る関数で使うと、かえって読みにくくなるケースがありました。
たとえば UIView.animate(withDuration:animations:completion:) のように完了ハンドラも受け取る版では、次のように 1 つ目のクロージャはラベル付きで括弧の中に残り、2 つ目だけが trailing closure として外に出る非対称な形になります。
UIView.animate(withDuration: 0.3, animations: {
self.view.alpha = 0
}) { _ in
self.view.removeFromSuperview()
}
この書き方は trailing closure の役割が読み取りにくく、最初のクロージャはネストしたまま残ります。結果として、Google の Swift Style Guide などではクロージャを複数渡す関数呼び出しで trailing closure を使うことが禁止される流れになっていました。
さらに、あとから完了ハンドラを追加したくなった場合、もとの呼び出しを trailing closure 形式から通常の引数形式へ書き直す必要があり、進行性のある disclosure(progressive disclosure)を意識した API 設計にとって摩擦が大きい状態でした。
// クロージャ引数が1つ → trailing closure
UIView.animate(withDuration: 0.3) {
self.view.alpha = 0
}
// クロージャ引数が複数 → trailing closure を使えない(従来の書き方)
UIView.animate(withDuration: 0.3, animations: {
self.view.alpha = 0
}, completion: { _ in
self.view.removeFromSuperview()
})
02 どのように解決されるのか
trailing closure syntax を拡張し、最初のラベルなしクロージャに続けて、ラベル付きのクロージャを任意個数 並べられるようにします。
// 単一の trailing closure
UIView.animate(withDuration: 0.3) {
self.view.alpha = 0
}
// 複数の trailing closure
UIView.animate(withDuration: 0.3) {
self.view.alpha = 0
} completion: { _ in
self.view.removeFromSuperview()
}
構文ルールは次のとおりです。
- 最初の trailing closure は、従来どおり 引数ラベルを省略 します。
- 2 つ目以降の trailing closure は、引数ラベルの記述が必須 です。
これにより、あとから完了ハンドラを追加するときも、先頭のクロージャはそのまま残して末尾に completion: などを足すだけで済みます。
API デザインガイドラインの更新
この構文と合わせて、Swift API Design Guidelines に次のガイダンスが追加されます。
関数名は、最初の trailing closure の引数ラベルが省略されることを前提に命名する。2 つ目以降の trailing closure には意味のある引数ラベルを付ける。
最初の trailing closure にラベルを付けることは許されません。許すとケースバイケースで判断する必要が生じ、結局 linter やスタイルガイドで禁止されることが目に見えているためです。
利用例: progressive disclosure を意識した API
複数の trailing closure を使うと、メインとなる必須クロージャと、オプショナルな補助的クロージャとを自然に書き分けられます。Combine の sink は、従来 receiveCompletion: を先に書かなければならず不格好でしたが、新しい構文では値を受け取るほうを先頭の trailing closure にでき、receiveCompletion: を末尾に添える形にできます。
ipAddressPublisher
.sink { identity in
self.hostnames.insert(identity.hostname!)
} receiveCompletion: { completion in
// handle error
}
SwiftUI の Section のように、必須のコンテンツに加えてオプショナルな header や footer を取るビューも、次のように統一的に書けるようになります。
Section {
// content
} header: {
// header
} footer: {
// footer
}
ラベル付き trailing closure のマッチング規則
ラベル付きの trailing closure は、対応するパラメータのラベルと一致する必要 があります。ラベルなしパラメータを指すための特別なラベルとして _: も使えますが、これは「ラベルがないパラメータ」とのみ一致し、ラベル省略の特権は持ちません。
func pointFromClosures(
x: () -> Int,
_ y: () -> Int
) -> (Int, Int) {
(x(), y())
}
pointFromClosures { 10 } _: { 20 } // OK
func performAsync(
action: @escaping () -> Void,
completionOnMainThread: @escaping () -> Void
) { ... }
performAsync {
// some action
} _: { // NG: completionOnMainThread: と書かなければならない
window.exit()
}
型検査のルール(後方スキャン)
型検査は従来の「末尾の 1 つだけ」を拡張したルールで行われます。具体的には、まずパラメータリストを 後ろから前に向かってスキャン し、ラベル付きの trailing closure をラベル一致でパラメータに束縛していきます。そのうえで、最初のラベルなし trailing closure は、最後に一致したラベル付きパラメータより前の区間に対して、従来と同じ限定的な後方スキャンで一致するパラメータを探します。
たとえば次の関数について、
func when<T>(
_ condition: @autoclosure () -> Bool,
then: () -> T,
`else`: () -> T
) -> T { ... }
新構文での呼び出しは、
when(2 < 3) {
print("then")
} else: {
print("else")
}
従来構文の次の呼び出しと等価です。
when(2 < 3, then: { print("then") }, else: { print("else") })
デフォルト引数との相互作用の注意点
trailing closure とデフォルト引数の関係は、従来の振る舞いがそのまま維持されます。そのため、必須の主クロージャと、デフォルト値付きのオプショナルなクロージャとを組み合わせた API は、期待どおりには型検査されません。
func resolve(
id: UUID,
action: (Object) -> Void,
completion: (() -> Void)? = nil,
onError: ((Error) -> Void)? = nil
) { ... }
// 期待したようには型検査されない
resolve(id: paulID) { paul in
// action
} onError: { error in
// handle error
}
これは、オプショナルなクロージャを両方省略した場合に「既存の単一 trailing closure の呼び出し」になってしまい、ソース互換性を壊さずにルールを変えられないためです。当面の回避策として、ライブラリ設計側ではデフォルト引数ではなく オーバーロード でこの形を表現することが推奨されます(型検査ルール自体を見直すことは、将来のソース互換モードで検討されうる方向性として示されています)。
default: の扱い
ラベル付き trailing closure の構文と switch 文の default: ラベルとの曖昧さを避けるため、エスケープされていない default キーワードは trailing closure のラベルとして使えません。どうしても使いたい場合は `default`: とバッククォートでエスケープする必要があります。