Swift Digest
SE-0081 | Swift Evolution

Move where clause to end of declaration

Proposal
SE-0081
Authors
David Hart, Robert Widmann, Pyry Jahkola
Review Manager
Chris Lattner
Status
Implemented (Swift 3.0)

01 何が問題だったのか

ジェネリクスを持つ関数や型の宣言では、型パラメータに対する制約を where 節で書きます。Swift 2 までは、この where 節はジェネリックパラメータリスト(<...>)の内側に置く構文になっていました。

// Swift 2 まで
func anyCommonElements<T : SequenceType, U : SequenceType where
    T.Generator.Element: Equatable,
    T.Generator.Element == U.Generator.Element>(lhs: T, _ rhs: U) -> Bool
{
    // ...
}

宣言の形が途中で分断される

制約がひとつふたつのうちはまだしも、複数の要件が絡むジェネリック関数では where 節が長くなります。そのすべてが <> のあいだに入るため、「関数名 → 型パラメータ → 引数リスト → 戻り値の型」という宣言の主要な流れが、長大な where 節によって真ん中で分断されてしまいます。引数リストや戻り値の型を読むためには、まず長い制約群を読み飛ばさなければなりません。

// 引数リスト (lhs: T, _ rhs: U) と戻り値 -> Bool が、
// 制約の塊の後ろに追いやられてしまう
func anyCommonElements<T : SequenceType, U : SequenceType where
    T.Generator.Element: Equatable,
    T.Generator.Element == U.Generator.Element>(lhs: T, _ rhs: U) -> Bool

整形しづらい

where 節を複数行に折り返しても、<...> の内側に閉じ込められている関係で、型パラメータ部分と制約部分、そして引数リストの開き括弧の位置を気持ちよく揃えるのが難しく、どう改行しても読みやすい形に落ち着きにくいという問題がありました。

エクステンションだけ構文が違う

制約付きのエクステンション(extension Array where Element: Equatable { ... })では、もともと where 節が宣言本体の直前、{ の手前に置かれます。ところが関数や型の宣言では <...> の内側に書かされるため、同じ「制約を書く構文」であるにもかかわらず、宣言の種類によって where 節の位置が違うという非対称が生じていました。

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

ジェネリックパラメータリスト(<...>)の内側からは要件(requirement)を取り除き、where 節を宣言の末尾(本体 { ... } の直前)に書くように構文を変更します。Swift 3 で実装されています。

// Swift 3 以降
func anyCommonElements<T : SequenceType, U : SequenceType>(lhs: T, _ rhs: U) -> Bool where
    T.Generator.Element: Equatable,
    T.Generator.Element == U.Generator.Element
{
    // ...
}

<...> にはシンプルな継承制約(T : SequenceType のような、型パラメータに対するプロトコルやスーパークラスの指定)だけが残り、複雑な要件はすべて where 節側にまとまります。関数名・型パラメータ・引数リスト・戻り値の型という宣言の骨格が途切れずに読める形になります。

適用される宣言

この構文変更は where 節を書きうるほぼすべての宣言に適用されます。関数・メソッド、イニシャライザ、struct / class / enum の宣言、プロトコルのメソッドやイニシャライザ要件などです。エクステンション(extension)はもともと宣言の末尾に where を書く形だったため影響を受けず、他の宣言の構文がエクステンション側に揃う形になります。

// 型宣言でも同様に末尾に where を置く
struct Pair<A, B> where A: Equatable, B: Equatable {
    let first: A
    let second: B
}

// プロトコル要件でも同じ
protocol Container {
    associatedtype Element
    func allEqual<Other>(to other: Other) -> Bool where
        Other: Sequence,
        Other.Iterator.Element == Element
}

シンプルな継承制約も where 節に移せる

<...> の中には引き続き T : SequenceType のような継承制約を書けますが、この変更によって、継承制約も含めてすべての要件を where 節側に寄せることもできるようになります。制約が多いときに <...> を型パラメータの列挙だけに留め、条件は末尾の where にまとめて書くスタイルが選べます。

func anyCommonElements<T, U>(lhs: T, _ rhs: U) -> Bool where
    T : SequenceType,
    U : SequenceType,
    T.Generator.Element: Equatable,
    T.Generator.Element == U.Generator.Element
{
    // ...
}

既存コードの移行

<...> の内側に where を書く既存の構文は使えなくなるため、where 節を使っているすべての宣言(エクステンションを除く)に影響が出ます。移行自体は where ... の部分を > の外、宣言の末尾に移すだけの機械的な書き換えで済み、コンパイラの Fix-It によって自動変換が提供されます。