Разделите seq предикатом" windowing " в Clojure
Я хотел бы" разбить " seq на подзапросы так же, как и partition-by, за исключением того, что функция применяется не к каждому отдельному элементу, а к диапазону элементов.
так, например:
(gather (fn [a b] (> (- b a) 2))
[1 4 5 8 9 10 15 20 21])
в результате:
[[1] [4 5] [8 9 10] [15] [20 21]]
дополнительно:
(defn f [a b] (> (- b a) 2))
(gather f [1 2 3 4]) ;; => [[1 2 3] [4]]
(gather f [1 2 3 4 5 6 7 8 9]) ;; => [[1 2 3] [4 5 6] [7 8 9]]
идея заключается в том, что я применяю начало списка и следующий элемент к функции, и если функция возвращает true, мы разделяем текущую головку списка до этой точки в новый раздел.
Я написал это:
(defn gather
[pred? lst]
(loop [acc [] cur [] l lst]
(let [a (first cur)
b (first l)
nxt (conj cur b)
rst (rest l)]
(cond
(empty? l) (conj acc cur)
(empty? cur) (recur acc nxt rst)
((complement pred?) a b) (recur acc nxt rst)
:else (recur (conj acc cur) [b] rst)))))
и это работает, но я знаю, что есть более простой способ. Мой вопрос:
есть ли встроенная функция для этого, где эта функция будет не нужна? Если нет, есть ли более идиоматическое (или более простое) решение, которое я пропустил? Что-то, сочетающее сокращение и взятие?
спасибо.
5 ответов
оригинальная интерпретация вопроса
мы (все), казалось, неправильно истолковали ваш вопрос как желание начать новый раздел всякий раз, когда предикат проводится для последовательных элементов.
еще один, ленивый, построенные на partition-by
(defn partition-between [pred? coll]
(let [switch (reductions not= true (map pred? coll (rest coll)))]
(map (partial map first) (partition-by second (map list coll switch)))))
(partition-between (fn [a b] (> (- b a) 2)) [1 4 5 8 9 10 15 20 21])
;=> ((1) (4 5) (8 9 10) (15) (20 21))
Вопрос
фактический вопрос просит нас начать новый раздел всякий раз, когда pred?
для начало текущего раздела и текущего элемент. Для этого мы можем просто оторвать partition-by
С несколькими настройками к его источнику.
(defn gather [pred? coll]
(lazy-seq
(when-let [s (seq coll)]
(let [fst (first s)
run (cons fst (take-while #((complement pred?) fst %) (next s)))]
(cons run (gather pred? (seq (drop (count run) s))))))))
(gather (fn [a b] (> (- b a) 2)) [1 4 5 8 9 10 15 20 21])
;=> ((1) (4 5) (8 9 10) (15) (20 21))
(gather (fn [a b] (> (- b a) 2)) [1 2 3 4])
;=> ((1 2 3) (4))
(gather (fn [a b] (> (- b a) 2)) [1 2 3 4 5 6 7 8 9])
;=> ((1 2 3) (4 5 6) (7 8 9))
поскольку вам нужно иметь информацию из предыдущих или следующих элементов, чем тот, который вы в настоящее время решаете, a partition
пар с reduce
может сделать трюк в этом случае.
это то, что я придумал после некоторых итераций:
(defn gather [pred s]
(->> (partition 2 1 (repeat nil) s) ; partition the sequence and if necessary
; fill the last partition with nils
(reduce (fn [acc [x :as s]]
(let [n (dec (count acc))
acc (update-in acc [n] conj x)]
(if (apply pred s)
(conj acc [])
acc)))
[[]])))
(gather (fn [a b] (when (and a b) (> (- b a) 2)))
[1 4 5 8 9 10 15 20 21])
;= [[1] [4 5] [8 9 10] [15] [20 21]]
основная идея состоит в том, чтобы сделать разделы из числа элементов, которые принимает функция предиката, заполнив последний раздел nil
s при необходимости. Затем функция уменьшает каждую секцию, определяя, предикат выполняется, если это так, то первый элемент в разделе добавляется в текущую группу и создается новая группа. Поскольку последний раздел мог быть заполнен нулями, предикат должен быть изменен.
Tow возможные улучшения этой функции будут заключаться в том, чтобы позволить пользователю:
- определите значение для заполнения последнего раздела, чтобы функция уменьшения могла проверить, является ли какой-либо из элементов раздела этим значением.
- указать арность предиката, что позволяет определить группировку с учетом текущего и последующих n элементов.
я написал некоторое время назад полезное:
(defn partition-between [split? coll]
(lazy-seq
(when-let [[x & more] (seq coll)]
(lazy-loop [items [x], coll more]
(if-let [[x & more] (seq coll)]
(if (split? [(peek items) x])
(cons items (lazy-recur [x] more))
(lazy-recur (conj items x) more))
[items])))))
он использует lazy-loop
, который просто способ написать lazy-seq
выражения, которые выглядят как loop/recur
, но я надеюсь, что это довольно ясно.
Я связался с исторической версией функции, потому что позже я понял, что есть более общая функция, которую вы можете использовать для реализации partition-between
или partition-by
, или действительно много других последовательных функций. В эти дни реализация много короче!--10-->, но менее очевидно, что происходит, если вы не знакомы с более общей функцией, которую я назвал glue
:
(defn partition-between [split? coll]
(glue conj []
(fn [v x]
(not (split? [(peek v) x])))
(constantly false)
coll))
обратите внимание, что оба этих решения ленивы, что в то время, когда я пишу это, не относится ни к одному из других решений в этом потоке.
вот один из способов, с разделением шагов. Его можно сузить до меньшего количества утверждений.
(def l [1 4 5 8 9 10 15 20 21])
(defn reduce_fn [f x y]
(cond
(f (last (last x)) y) (conj x [y])
:else (conj (vec (butlast x)) (conj (last x) y)) )
)
(def reduce_fn1 (partial reduce_fn #(> (- %2 %1) 2)))
(reduce reduce_fn1 [[(first l)]] (rest l))
keep-indexed это замечательная функция. Учитывая функцию f
и вектора lst
,
(keep-indexed (fn [idx it] (if (apply f it) idx))
(partition 2 1 lst)))
(0 2 5 6)
это возвращает индексы, после который вы хотите разделить. Давайте увеличим их и поставим 0 спереди:
(cons 0 (map inc (.....)))
(0 1 3 6 7)
Раздел для диапазонов:
(partition 2 1 nil (....))
((0 1) (1 3) (3 6) (6 7) (7))
теперь используйте их для генерации subvecs:
(map (partial apply subvec lst) ....)
([1] [4 5] [8 9 10] [15] [20 21])
собираем все вместе:
(defn gather
[f lst]
(let [indices (cons 0 (map inc
(keep-indexed (fn [idx it]
(if (apply f it) idx))
(partition 2 1 lst))))]
(map (partial apply subvec (vec lst))
(partition 2 1 nil indices))))
(gather #(> (- %2 %) 2) '(1 4 5 8 9 10 15 20 21))
([1] [4 5] [8 9 10] [15] [20 21])