Sequenceの終端操作名を合理化する
Rationalizing Sequence end-operation names
このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら↗。
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 です。
03 今後の見通し
本提案では、シーケンスの端を扱う API 群の体系化に伴って多数の追加・改名アイデアが派生的に検討されました。Proposal の Future Directions では、これらをまとめて「かなり初期段階のアイデア」だと断った上で列挙しています。いずれも将来の構想として示されたものであり、実現を約束するものではありません。
さらなる API のリネーム候補
提案本体のリネームに加えて、ソース互換性に与える影響の大きさを段階的に分けたリネーム案も挙げられていました。
- 大きな影響:
map/flatMap/filter/reduceの名前が Swift の命名規約に十分沿っているか、またクロージャ引数のラベルを見直すかどうかの議論。後述のevery(where:)をfilterの代替名として位置づける案も含まれます。 - それなりの影響:
removeSubrange(_:)/replaceSubrange(_:with:)をremove(in:)/replace(in:with:)のように改名する案。また、removingFirst()/removingLast()はremovingPrefix(1)/removingSuffix(1)と完全に等価で、対応する取得側 API がSequenceではなくCollection側にしかないため、思い切って削除する案。 - 小さな影響:
removeFirst()/removeLast()とpopFirst()/popLast()は、空コレクションでの挙動と返り値がOptionalかどうかの違いしかなく、ほぼ重複しているため、これらを統合する案。あわせてremovePrefix(_:)/removeSuffix(_:)から「対象要素が存在すること」を要求する事前条件を外し、removing系と並行な仕様にそろえることも検討されています。事前条件のチェックを避けたい場合は、remove(at:)やremoveSubrange(_:)を使えばよいという整理です。
シーケンスの端を扱う API の穴埋め
リネームによって first / last / prefix / suffix を軸にした表が描けるようになったため、その表の空欄を埋める形でいくつもの追加 API が考えられます。Proposal は厳密な有用性の精査までは行わず、整合的に成立する候補をひと通り並べたうえで、優先度を次のように整理しています。
- 特に有望なもの
first/prefix系のメソッドに対応するlast/suffix系を一通り追加する- 適切な prefix / suffix 系 API すべてに
while:版を追加する
- 状況によっては有用だが、組み合わせで代用しやすいもの
- 内容指定で除去・非破壊除去を行う
remove/removing系 prefixIndex(while:)/suffixIndex(while:)
- 内容指定で除去・非破壊除去を行う
- 利便性にとどまり、強い動機付けが見えにくいもの
firstIndex/lastIndexの単純形やprefixIndex(_:)/suffixIndex(_:)first(_:)/last(_:)(指定値と等しい最初・最後の要素を返す形)
ここで prefixIndex / suffixIndex は、それぞれ prefix の末尾の index、suffix の先頭の index を返すイメージで構想されており、Indices を付けて範囲を返す設計に振る選択肢にも触れられています。
all / every を軸にした行の追加
上記の表に「全体(all)」と「条件に合うすべて(every matching)」の行を加える方向も示されています。これにより、新しい API を追加するだけでなく、既存 API の改名についても示唆が得られます。
indicesをallIndicesと呼び直す案- 既存の
removeAll()をこの軸の中に位置づける案 filterをevery(where:)と呼び直す案。filterは強い term of art ではあるものの、テストの向きに関する誤解(条件に合うものを残すのか除くのか)が起きにくい点が利点として挙げられています
every 以外に all や any を軸にした命名も考えられるものの、対応する first / last 系メソッドと並行な命名パターンを保ったまま自然な英語にしようとすると無理が出やすい、という整理になっています。
これらはいずれも、本提案の延長線上で検討する価値があるアイデアとして列挙されたものにすぎず、具体的な API としての採用が約束されているわけではありません。