Swift Digest
SE-0045 | Swift Evolution

Add prefix(while:) and drop(while:) to the stdlib

Proposal
SE-0045
Authors
Lily Ballard
Review Manager
Chris Lattner
Status
Implemented (Swift 3.1)

01 何が問題だったのか

Swiftの標準ライブラリには、シーケンスを操作するための便利なメソッドが一通りそろっていました。先頭から n 個を捨てる dropFirst(_:) や、先頭 n 個を取り出す prefix(_:)、条件で要素を選ぶ filter(_:) などです。しかし、「条件を満たしている間だけ捨てる/取り出す」という、関数型言語ではおなじみの操作に対応するものがありませんでした。

実際の処理では、「先頭の空白や空行をスキップして、そこからの内容をまとめて扱いたい」「昇順に並んだ値のうち、ある閾値を超えるまでの連続した範囲だけを切り出したい」といった場面がよくあります。このとき、filter(_:) では「シーケンス全体をなめて条件に合うものを集める」動きになってしまうため、「先頭から条件が崩れた時点で止めたい」という意図とは合いません。

let numbers = [1, 2, 3, 4, 5, 6, 2, 1]

// filter だと末尾側の 2, 1 まで拾ってしまう
numbers.filter { $0 < 4 }
// [1, 2, 3, 2, 1]

// 本当は「先頭から 4 未満が続く範囲」だけが欲しい → [1, 2, 3]

結果として、利用者は毎回インデックスと for ループを組み合わせた手書きのコードを書く羽目になっていました。dropFirst(_:) / prefix(_:) のように「件数」を渡すAPIはあっても、「条件」を渡すAPIが無いというのは、標準ライブラリの表現力の穴になっていました。

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

Sequence に、述語を受け取る2つのメソッド prefix(while:)drop(while:) を追加します。

  • prefix(while:): 先頭から、述語が true を返す間の要素を取り出した部分シーケンスを返します。false になった時点以降は含めません。
  • drop(while:): 先頭から、述語が true を返す間の要素を読み飛ばし、初めて false になった要素以降の部分シーケンスを返します。

prefix(_:)dropFirst(_:) の「件数」版と対になる、「条件」版の操作と考えると自然です。

使い方

let numbers = [1, 2, 3, 4, 5, 6, 2, 1]

// 先頭から 4 未満が続く範囲だけ
numbers.prefix(while: { $0 < 4 })
// [1, 2, 3]

// 先頭の「4 未満が続く範囲」を読み飛ばした残り
numbers.drop(while: { $0 < 4 })
// [4, 5, 6, 2, 1]

filter(_:) と違い、どちらも「条件が崩れた時点で打ち切る」ため、末尾側にある条件一致要素を拾ってしまうことはありません。

ソート済みのデータから特定の範囲だけを取り出す用途にも向いています。

let sorted = [1, 3, 5, 8, 13, 21, 34]

// 10 未満が続く間だけ取る
sorted.prefix(while: { $0 < 10 })
// [1, 3, 5, 8]

// 10 未満を読み飛ばした残り
sorted.drop(while: { $0 < 10 })
// [13, 21, 34]

述語は throws に対応しているため、エラーを投げるクロージャも渡せます(呼び出し側は try が必要です)。

戻り値と Collection での挙動

Sequence でのデフォルト実装は AnySequence でラップした結果を返します。これは、メソッドの呼び出し後にクロージャを保持し続けないという標準ライブラリの方針にそろえるためで、内部的には一度 Array に取り出したうえで包んで返す実装になっています。

Collection にはオーバーライドが用意されており、元のコレクションのスライス(SubSequence)を返します。つまり ArrayString のような具体的なコレクションに対して呼び出すと、新しいストレージを確保せずに部分範囲を指す軽量な値が得られます。

let array = [1, 2, 3, 4, 5]
let head = array.prefix(while: { $0 < 4 })
// head は ArraySlice<Int>([1, 2, 3] を指すスライス)

lazy を介した遅延版

LazySequenceProtocolLazyCollectionProtocol にも専用のオーバーロードが追加されます。lazy を挟んだシーケンス/コレクションに対して呼び出すと、即時評価ではなく遅延評価される専用の型(LazyPrefixWhileSequence / LazyDropWhileSequence、コレクション版は LazyPrefixWhileCollection / LazyDropWhileCollection)が返ります。

let results = (1...1_000_000)
  .lazy
  .prefix(while: { $0 < 10 })

// この時点ではまだ計算されない
for x in results {
  print(x)
}
// 1, 2, 3, 4, 5, 6, 7, 8, 9

ほかの遅延版の演算と同じく、実際の要素は取り出されるタイミングで必要な分だけ計算されます。遅延版 drop(while:) のコレクション版は、startIndex にアクセスした時点で「読み飛ばす範囲」の計算を行う点が特徴です(遅延版 filter(_:) と同様の振る舞いです)。

補足: 提案の経緯

元の提案には scan(_:combine:)unfold(_:applying:) も含まれていましたが、レビューの結果、scan は有用性が低いとして、unfold は命名上の問題として却下され、prefix(while:)drop(while:) のみが採用されました(標準ライブラリへの採用は Swift 3.1)。