これらのシナリオ (つまり、1 つの入力シーケンスで複数の出力シーケンスを生成する必要があるシナリオ) で使用する経験則では、次の 3 つの望ましいプロパティのうち、一般的には 2 つしか持つことができません。
- 効率(入力シーケンスを 1 回だけトラバースするため、頭を保持しない)
- 怠惰 (オンデマンドでのみ要素を生成する)
- 共有可変状態なし
clojure.core のバージョンは (1,3) を選択しますが、パーティション全体を一度に作成することで (2) をあきらめます。Python と Haskell はどちらも (1,2) を選択しますが、すぐにはわかりません: Haskell には変更可能な状態がまったくないのではないでしょうか? まあ、すべての (シーケンスだけでなく) の遅延評価は、すべての式がthunksであることを意味します。これは、空白のスレートとして始まり、値が必要な場合にのみ書き込まれます。あなたが言うように、の実装は両方の出力シーケンスでspan
同じサンクを共有するspan p xs'
ため、最初にそれを必要とする方が他のシーケンスの結果にそれを「送信」し、保存するために必要な距離でアクションを実行しますその他の素晴らしいプロパティ。
あなたが指摘したように、あなたがリンクした代替Clojure実装は(2,3)を選択します。
問題は、 (1)またはpartition-by
(2)のいずれかを拒否することは、入力または出力のいずれかのシーケンスの先頭を保持していることを意味することです。したがって、任意に大きな入力の任意に大きなパーティションを処理できるソリューションが必要な場合は、(1,2) を選択する必要があります。Clojure でこれを行う方法はいくつかあります。
- Python のアプローチをとってください: seq よりもイテレータに似たものを返します。 1 つのイテレータからアイテムを消費すると、他のイテレータを自由に変更または無効化できます。これにより、消費が順番に行われ、メモリを解放できることが保証されます。
- Haskell のアプローチを採用します。手動ですべてをサンクし、大量の への呼び出しを
delay
行い、データを取得するためにクライアントがforce
必要なだけ呼び出す必要があります。これは Clojure ではかなり醜くなり、スタックの深さが大幅に増加します (重要な入力でこれを使用すると、おそらくスタックが壊れます) が、理論的には可能です。
- 出力 seq 間で調整され、いずれかから何かが要求されたときに必要に応じてそれぞれが更新されるいくつかの変更可能なデータ オブジェクトを使用することにより、Clojure 風に (しかしまだ非常に珍しい) 何かを記述します。
これら 3 つのアプローチのいずれかが可能であると確信していますが、正直なところ、どれも非常に難しく、まったく自然ではありません。Clojure のシーケンスの抽象化では、希望するデータ構造を簡単に作成することはできません。私のアドバイスは、このようなものが必要で、パーティションが大きすぎて快適に収まらない場合は、わずかに異なる形式を受け入れて、自分でもう少し簿記を行うことです。すべての出力シーケンス!
したがって((2 4 6 8) (1 3 5) (10 12) (7))
、 のような出力形式の代わりに(partition-by even? [2 4 6 8 1 3 5 10 12 7])
、少し醜い形式を受け入れることができます([::key true] 2 4 6 8 [::key false] 1 3 5 [::key true] 10 12 [::key false] 7)
。これは作成するのも消費するのも難しくありませんが、書き出すのは少し長く退屈です。
プロデュース関数の合理的な実装の 1 つを次に示します。
(defn lazy-partition-by [f coll]
(lazy-seq
(when (seq coll)
(let [x (first coll)
k (f x)]
(list* [::key k] x
((fn part [k xs]
(lazy-seq
(when (seq xs)
(let [x (first xs)
k' (f x)]
(if (= k k')
(cons x (part k (rest xs)))
(list* [::key k'] x (part k' (rest xs))))))))
k (rest coll)))))))
reduce-grouped
これを使用する方法は次のとおりです。最初にグループ化形式の詳細を非表示にするジェネリックを定義し、次に、count-partition-sizes
シーケンスをメモリに保持せずに各パーティションのキーとサイズを出力する関数の例を示します。
(defn reduce-grouped [f init groups]
(loop [k nil, acc init, coll groups]
(if (empty? coll)
acc
(if (and (coll? (first coll)) (= ::key (ffirst coll)))
(recur (second (first coll)) acc (rest coll))
(recur k (f k acc (first coll)) (rest coll))))))
(defn count-partition-sizes [f coll]
(reduce-grouped (fn [k acc _]
(if (and (seq acc) (= k (first (peek acc))))
(conj (pop acc) (update-in (peek acc) [1] inc))
(conj acc [k 1])))
[] (lazy-partition-by f coll)))
user> (lazy-partition-by even? [2 4 6 8 1 3 5 10 12 7])
([:user/key true] 2 4 6 8 [:user/key false] 1 3 5 [:user/key true] 10 12 [:user/key false] 7)
user> (count-partition-sizes even? [2 4 6 8 1 3 5 10 12 7])
[[true 4] [false 3] [true 2] [false 1]]
編集:もう一度見てみると、キーがいつ変更されるかを明確に示すことができないため、 myreduce-grouped
が よりもはるかに便利であるとは確信し(reduce f init (map g xs))
ていません。lazy-partition-by
したがって、グループがいつ変更されるかを知る必要がある場合は、よりスマートな抽象化が必要になるか、 「巧妙な」ラッピングを何もせずに私のオリジナルを使用する必要があります。