Rationalizing Sequence end-operation names
01 何が問題だったのか
Sequence と Collection には、先頭・末尾の要素やその近辺を取り出したり取り除いたりするための API が数多く用意されています。ところが、これらの名前は歴史的な経緯でばらばらに付けられており、関連する操作を関連する名前で見つけにくいという問題がありました。
似た操作で単語の使い分けが揃っていない
先頭や末尾から「個数指定」で取り出す系の API には次のようなものがあります。
Sequence.prefix(_:)/Sequence.suffix(_:)Sequence.prefix(while:)String.hasPrefix(_:)/String.hasSuffix(_:)
一方、先頭・末尾から要素を取り除く API では first / last が使われます。
Sequence.dropFirst(_:)/Sequence.dropLast(_:)Sequence.removeFirst(_:)/Sequence.removeLast(_:)
さらに、どちらの単語も使わない API もあります。
Sequence.starts(with:)Sequence.drop(while:)
本来は同じファミリーであるはずの操作が、名前を頼りに探しても揃って出てこない状態でした。
first の意味が一貫していない
first という単語は、これらの API の中で 3 通りの意味で使われていました。
- 「シーケンスの先頭要素そのもの」(
Collection.first) - 「先頭から一定個数を含む部分列」(
dropFirst(3)のFirst) - 「条件に合う最初の要素」(
first(where:))
返り値の型も意味も異なるのに同じ単語が使い回されており、読者にとって紛らわしい状態でした。
drop が誤解を招く
drop は関数型言語由来の用語で、非破壊的に「先頭から n 個を除いた部分列」を返します。しかし Swift の命名ガイドラインから見ると、
- 非破壊的な操作でありながら
-ing/-ed形になっていない - SQL の
DROP TABLEのように破壊的操作を連想させる dropFirstとdropLastがprefix/suffixファミリーから外れてしまう
といった問題があり、特に破壊的に見えてしまう点は初心者を戸惑わせる原因でした。
方向が名前から読み取れない
Sequence.drop(while:) や Collection.index(of:) は、先頭・末尾どちらから走査するかによって結果が変わり得る操作ですが、名前にその方向が現れていませんでした。そのため、将来 lastIndex(of:) のような対称 API を追加しようとしたときに、名前の付け方が噛み合わない懸念がありました。
インデックス指定の API が prefix / suffix と混ざっている
prefix(upTo:) / prefix(through:) / suffix(from:) は、一見すると prefix(_:) / suffix(_:) の仲間に見えますが、実際にはインデックスで範囲を切り出すスライス操作であり、個数指定の API とは性質が違います。特に Int インデックスの Array では、
prefix(3)とprefix(upTo: 3)がたまたま同じ結果を返す- 一方で
suffix(3)とsuffix(from: 3)はまったく別物
となるため、suffix(from:) の引数が「末尾からの個数」ではなく「先頭基準のインデックス」であることが誤解されやすい、という問題がありました。
これらの事情から、シーケンスの端を扱う API 群全体を、予測しやすい名前の体系へ整理し直すことが提案されました。
02 どのように解決されるのか
この提案は、機能の追加・削除は行わず、既存 API のリネームと再設計のみを行うものでした。端点を扱う操作を「シーケンスの端を操作する API」と「インデックスで範囲を切り出す API」の 2 つのファミリーに分け、それぞれに一貫した命名規則を与えます。
なお、本提案は最終的に Rejected となりました。以下は提案された内容の要点です。
シーケンスの端を操作する API のリネーム
個数指定や条件指定の端点操作は、次の方向を表す単語を一貫して使うように揃えます。
| 操作対象 | 使う単語 |
|---|---|
| 先頭 1 要素 | first |
| 末尾 1 要素 | last |
先頭 n 要素(固定個数や while:) |
prefix |
末尾 n 要素(固定個数や while:) |
suffix |
| 条件に合う最初の要素 | first |
| 条件に合う最後の要素 | last |
これに合わせて、starts(with:) は hasPrefix(_:) に、drop 系のメソッドは removing 系にリネームされます。removing は「remove と同じ要素を取り除いた結果を、元の値を変えずに返す」ことを表す形容詞的な命名です(返り値は Self ではなく SubSequence のままですが、対応関係を優先しています)。
主なリネームは次の通りです。
| 旧名 | 新名 |
|---|---|
dropFirst() |
removingFirst() |
dropLast() |
removingLast() |
dropFirst(_:) |
removingPrefix(_:) |
drop(while:) |
removingPrefix(while:) |
dropLast(_:) |
removingSuffix(_:) |
removeFirst(_:) |
removePrefix(_:) |
removeLast(_:) |
removeSuffix(_:) |
starts(with:) |
hasPrefix(_:) |
index(of:) |
firstIndex(of:) |
index(where:) |
firstIndex(where:) |
使用例は次のようになります。
let numbers = [1, 2, 3, 4, 5]
// 先頭 2 つを除いた部分列を得る(旧: dropFirst(2))
let tail = numbers.removingPrefix(2) // [3, 4, 5]
// 末尾 2 つを除いた部分列を得る(旧: dropLast(2))
let head = numbers.removingSuffix(2) // [1, 2, 3]
// 先頭に [1, 2] が並んでいるかを確かめる(旧: starts(with:))
if numbers.hasPrefix([1, 2]) {
// ...
}
// 最初に 3 以上の要素が現れる位置(旧: index(where:))
let i = numbers.firstIndex(where: { $0 >= 3 })
removeFirst(_:) / removeLast(_:) のように破壊的な API も、複数要素版は removePrefix(_:) / removeSuffix(_:) へ改称され、単一要素版の removeFirst() / removeLast() と名前空間上も分けられます。
インデックス指定の API をスライス構文へ統一
prefix(upTo:) / prefix(through:) / suffix(from:) の 3 つは、サブスクリプトによるスライス構文に置き換えます。そのために、上下どちらか片方の境界を省略できる範囲表現を新しく導入します。
let people = ["Alice", "Bob", "Carol", "Dave"]
let i = 2
// 先頭から i の手前まで(旧: people.prefix(upTo: i))
let head = people[..<i] // ["Alice", "Bob"]
// 先頭から i まで含む(旧: people.prefix(through: i))
let headIncl = people[...i] // ["Alice", "Bob", "Carol"]
// i から末尾まで(旧: people.suffix(from: i))
let tail = people[i..<] // ["Carol", "Dave"]
これを実現するため、次の仕組みが導入されます。
IncompleteRange<Bound>/IncompleteClosedRange<Bound>:片側または両側の境界がnilになり得る範囲型- 前置 / 後置の
..</...演算子:..<iやi..<の形で境界のみを指定して範囲を作る RangeExpressionプロトコル:任意の範囲的な型を、対象コレクションに対応するRange<Index>に変換するインターフェース
これらに合わせて、Collection のスライス用サブスクリプトは Range<Index> を取るものと RangeExpression を取るジェネリック版の 2 本に集約され、removeSubrange(_:) / replaceSubrange(_:with:) も RangeExpression を受け取れるようになります。これにより、
var nums = [10, 20, 30, 40, 50]
nums.removeSubrange(...2) // [40, 50]
nums.replaceSubrange(..<1, with: [0]) // [0, 50]
のようにサブスクリプトと同じ自然な範囲記法で破壊的操作も行えるようになります。既存の prefix(upTo:) / prefix(through:) / suffix(from:) は、冗長になるため削除されます。
なぜ Rejected になったか
本提案は Swift 3 のレビュー期間中に寄せられた議論の結果、Rejected(後に deferred とも整理)となりました。主な論点は、リネーム案そのものが純粋な改善といえるか議論が分かれたこと、および IncompleteRange 周辺の設計を Swift 3 の段階で固めるのは時期尚早だとされたことです。
Swift 標準ライブラリで実際に採用されたのは、本提案のうち firstIndex(of:) / firstIndex(where:) などごく一部の改名に限られます。dropFirst / dropLast / starts(with:) / prefix(upTo:) などは現在も従来の名前のまま残っており、境界省略の範囲記法(array[..<i] や array[i...] の形)はその後、別の形で(PartialRangeUpTo / PartialRangeFrom などとして)Swift 4 で導入されました。本提案は、そこに至る議論の出発点として位置づけられる Proposal です。