Swift Digest
SE-0118 | Swift Evolution

Closure Parameter Names and Labels

Proposal
SE-0118
Authors
Dave Abrahams, Dmitri Gribenko, Maxim Moiseev
Review Manager
Chris Lattner
Status
Implemented (Swift 3.0)

01 何が問題だったのか

Swift 標準ライブラリの API には、クロージャを引数にとるメソッドが数多くあります。たとえば sortminmaxsplitreducecontains などがそれにあたります。ところが、これらのクロージャ引数に付けられていた 引数ラベルパラメータ名 は、API ごとに場当たり的に決められてきた経緯があり、一貫性を欠いていました。

具体的には次のような問題がありました。

  • 引数ラベルがメソッドごとにばらばら(sort(isOrderedBefore:) / lexicographicallyPrecedes(_:isOrderedBefore:) / elementsEqual(_:isEquivalent:) / starts(with:isEquivalent:) など、比較用クロージャに付くラベルが統一されていない)。
  • ラベルがクロージャの セマンティクス(「どういう順序であるべきか」など)を言い当てようとするあまり冗長で、呼び出し側のコードとしては読みづらい。
  • trailing closure 記法を使うと引数ラベルは消えるため、ラベルの冗長さはそのままコードに残らないものの、trailing closure を使わない呼び出し、ドキュメント、コード補完など ラベルが表に出る文脈 ではやはり読みにくさとして現れる。
  • パラメータ名(ドキュメントやコード補完で表示される仮引数名)についても同様で、ばらつきがあって説明文や補完候補が読みにくくなる。

要するに、「クロージャ引数のラベルと仮引数名にまとまった指針がなく、API の一貫性・可読性・補完体験がそろって損なわれている」という状況でした。

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

標準ライブラリのクロージャ引数について、引数ラベルとパラメータ名を一括で見直し、より簡潔で一貫性のある形に揃えます。大まかな方針は次のとおりです。

  • クロージャの引数ラベルは、セマンティクスを逐語的に説明するのではなく、呼び出し側で自然に読める前置詞・短い語by:, where:, into: など)に寄せる。
  • 第一引数としてクロージャだけを受け取るメソッドは、ラベルを外して素直に渡せるようにする。
  • パラメータ名(仮引数名)もドキュメントや補完で読みやすい形に統一する。

主な変更例

変更前 変更後
words.sort(isOrderedBefore: >) words.sort(by: >)
words.sorted(isOrderedBefore: >) words.sorted(by: >)
shapes.min(isOrderedBefore: haveIncreasingSize) shapes.min(by: haveIncreasingSize)
shapes.max(isOrderedBefore: haveIncreasingSize) shapes.max(by: haveIncreasingSize)
a.lexicographicallyPrecedes(b, isOrderedBefore: haveIncreasingWeight) a.lexicographicallyPrecedes(b, by: haveIncreasingWeight)
expected.elementsEqual(actual, isEquivalent: haveSameValue) expected.elementsEqual(actual, by: haveSameValue)
names.starts(with: myFamily, isEquivalent: areSameExceptForCase) names.starts(with: myFamily, by: areSameExceptForCase)
lines.split(isSeparator: isAllWhitespace) lines.split(whereSeparator: isAllWhitespace)
roots.contains(isPrime) roots.contains(where: isPrime)
measurements.reduce(0, combine: +) measurements.reduce(0, +)
s.withUTF8Buffer(invoke: processBytes) s.withUTF8Buffer(processBytes)
UTF8.encode(scalar, sendingOutputTo: accumulateByte) UTF8.encode(scalar, into: accumulateByte)
ManagedBuffer<Header,Element>.create(minimumCapacity: 10, initialValue: makeHeader) ManagedBuffer<Header,Element>.create(minimumCapacity: 10, makingValueWith: makeHeader)

ポイントをいくつか補足します。

  • 比較用のクロージャは by: に統一: sort / sorted / min / max / lexicographicallyPrecedes / elementsEqual / starts(with:) など、順序や同値性を判定するクロージャを取るメソッドは軒並み by: で揃えます。これにより「何をもって並べるか/比較するか」を短く読める形になります。
  • 第一引数でラベル不要なケースはラベルを外す: reduce(0, combine: +)reduce(0, +) に、withUTF8Buffer(invoke:)withUTF8Buffer(_:) に変わります。
  • contains は述語用に where: を追加: 要素の等値判定を取る contains(_:) との混同を避けるため、述語(クロージャ)を取る形は contains(where:) という別シグネチャになります。
  • split の述語ラベルは whereSeparator:: 「区切り文字かどうかを判定するクロージャ」であることが読み取れるラベルに変わります。
  • 出力先クロージャは into:: UTF8.encode(_:sendingOutputTo:) のように「出力を送る先」を表していた長いラベルは、into: に短縮されます。

典型的な呼び出しの変化

並べ替えや検索では、多くの場合 trailing closure と組み合わせて使うため、呼び出し側の見た目はシンプルになります。

// ソート
words.sort(by: >)
let sorted = words.sorted { $0.count < $1.count }

// 最小値・最大値
let smallest = shapes.min(by: haveIncreasingSize)
let largest = shapes.max { $0.area < $1.area }

// 述語で検索
if roots.contains(where: isPrime) { ... }
if let first = numbers.first(where: { $0 > 10 }) { ... }

// 区切り判定
let parts = lines.split(whereSeparator: isAllWhitespace)

// reduce はラベルなしで
let sum = measurements.reduce(0, +)

既存コードへの影響

これは標準ライブラリ API のシグネチャを書き換える破壊的変更であり、旧来のラベルに依存したコードはそのままではコンパイルが通りません。ただし標準ライブラリ側で availability 注釈を付けることで、マイグレーションの過程でコンパイラが新しい綴りを案内できるようになっています。

今後の展望

この見直しの過程で、標準ライブラリの関数的メソッドには 関数型言語の慣習名 よりも Swift らしい名前のほうが読みやすいケースがあることも見えてきました。たとえば filter は、渡すクロージャが「真を返した要素を残す」方向の述語であることをはっきり示す意味で where のような名前のほうが polarity を誤解しにくく、reduce もアキュムレータを渡して合成していくというセマンティクスを考えると accumulated のような名前が候補になり得ます。こうしたメソッド名自体の変更は別の提案として検討される余地がある、という位置づけです(本提案の範囲では扱いません)。