Add last(where:) and lastIndex(where:) Methods
01 何が問題だったのか
SE-0032 で Sequence に first(where:) が、Swift 3 時点の標準ライブラリには Collection に index(where:) / index(of:) が用意されていました。しかし、これらはいずれも先頭から線形探索するメソッドで、末尾から探すための対になるメソッドは提供されていませんでした。
let a = [20, 30, 10, 40, 20, 30, 10, 40, 20]
a.first(where: { $0 > 25 }) // 30
a.index(where: { $0 > 25 }) // 1
a.index(of: 10) // 2
「末尾から条件を満たす最初の要素を見つけたい」というニーズは、たとえば長い文字列を最大長で折り返すときや、前後の空白を取り除くときなど、文字列処理を中心に数多くあります。それなのに標準ライブラリにはその手段がなく、利用者は reversed() と既存メソッドを組み合わせて書くしかありませんでした。
とくに「末尾から見て条件を満たす最初のインデックス」を求めたいときは、reversed() が返す Index が元のコレクションの Index とは別物になるため、base プロパティで元のインデックスへ戻したうえで、1つ前へずらすという処理が必要でした。その結果、次のような読みにくいコードを書かざるを得ませんでした。
(a.reversed().index(where: { $0 > 25 })?.base).map({ a.index(before: $0) })
また、既存の index(of:) / index(where:) という名前にも問題がありました。Collection には index(after:) や index(_:offsetBy:) のように「インデックスを操作する」ための index(...) メソッド群があり、「要素を探してインデックスを返す」系の index(of:) / index(where:) と同じ名前空間に同居していたため、役割の違いが名前からは読み取りにくい状態でした。末尾検索のメソッドを追加するなら、先頭検索のメソッドとの対称性も踏まえて整理したいところです。
02 どのように解決されるのか
末尾から検索するためのメソッドを標準ライブラリに追加し、既存の先頭検索メソッドの名前も整理して対称な形に揃えます。
追加されるメソッド
Sequence に last(where:) を、Collection に lastIndex(where:) と lastIndex(of:) を追加します。last(where:) は条件を満たす最後の要素を、lastIndex(where:) / lastIndex(of:) はそのインデックスを返します。いずれも見つからなければ nil を返します。
let a = [20, 30, 10, 40, 20, 30, 10, 40, 20]
a.last(where: { $0 > 25 }) // 40
a.lastIndex(where: { $0 > 25 }) // 7
a.lastIndex(of: 10) // 6
reversed() を介した回りくどい書き方が不要になり、末尾から探したいという意図が素直にコードに表れるようになります。
先頭検索メソッドのリネーム
対称性を保ち、index(...) をインデックス操作専用の名前空間として整理するため、既存の index(of:) / index(where:) はそれぞれ firstIndex(of:) / firstIndex(where:) に改名されます。first... と last... のペアで同じ役割のメソッドが並ぶ形になり、APIの構造が分かりやすくなります。
// 旧: index(of:) / index(where:)
// 新: firstIndex(of:) / firstIndex(where:)
a.firstIndex(where: { $0 > 25 }) // 1
a.firstIndex(of: 10) // 2
旧名は Swift 4.2 で deprecated 扱いとなり、Swift 5 で削除される予定でした。移行はマイグレータで機械的に行えます。
プロトコルごとの提供範囲とパフォーマンス
last(where:) と lastIndex(where:) はそれぞれ Sequence / Collection のプロトコル要件として宣言され、デフォルト実装が用意されます。BidirectionalCollection では末尾側から逆向きに辿れるため、より効率的なデフォルト実装が追加で提供されます。lastIndex(of:) は Element: Equatable という制約のもと、Collection と BidirectionalCollection の両方に拡張として提供されます。
双方向に辿れないシーケンスに対しても last(where:) を提供しているのは、Sequence / Collection 上の既存メソッドにも同じパフォーマンス特性のものがすでにあるためです(そもそも先頭から最後まで走査しなければ末尾が確定しないので、この計算量は避けられません)。利用者は BidirectionalCollection 以外で呼び出した場合、末尾まで走査が走る点だけ意識しておけば十分です。