代码之家  ›  专栏  ›  技术社区  ›  Alan Thompson

如何使用开始/停止谓词对列表的连续元素进行分组?

  •  4
  • Alan Thompson  · 技术社区  · 7 年前

    假设我有一个如下列表:

    (def data [:a :b :c :d :e :f :g :h :b :d :x])
    

    和谓词,如:

    (defn start? [x] (= x :b))
    (defn stop?  [x] (= x :d))
    

    (parse data) => [:a [:b :c :d] :e :f :g :h [:b :d] :x]
    

    如何使用Clojure完成此任务?

    7 回复  |  直到 7 年前
        1
  •  7
  •   exupero    7 年前

    您可以使用自定义有状态传感器:

    (defn subgroups [start? stop?]
      (let [subgroup (volatile! nil)]
        (fn [rf]
          (fn
            ([] (rf))
            ([result] (rf result))
            ([result item]
             (let [sg @subgroup]
               (cond
                 (and (seq sg) (stop? item))
                 (do (vreset! subgroup nil)
                   (rf result (conj sg item)))
                 (seq sg)
                 (do (vswap! subgroup conj item)
                   result)
                 (start? item)
                 (do (vreset! subgroup [item])
                   result)
                 :else (rf result item))))))))
    
    (into []
          (subgroups #{:b} #{:d})
          [:a :b :c :d :e :f :g :h :b :d :x])
    ; => [:a [:b :c :d] :e :f :g :h [:b :d] :x]
    
        2
  •  4
  •   Taylor Wood    7 年前

    我喜欢这个 stateful transducer answer 开始 元素,但未找到 停止 元素被找到。如果一个子组是左的 打开 停止 删除的元素:

    (into [] (subgroups #{:b} #{:d}) [:a :b :c :e :f :g :h :b :x])
    => [:a] ;; drops inputs from before (last) subgroup opens
    

    传感器有一个 完成

    本示例与原始传感器示例的唯一区别是 完成 数量:

    (defn subgroups-all [start? stop?]
      (let [subgroup (volatile! nil)]
        (fn [rf]
          (fn
            ([] (rf))
            ([result] ;; completing arity flushes open subgroup
             (let [sg @subgroup]
               (if (seq sg)
                 (do (vreset! subgroup nil)
                     (rf result sg))
                 (rf result))))
            ([result item]
             (let [sg @subgroup]
               (cond
                 (and (seq sg) (stop? item))
                 (do (vreset! subgroup nil)
                     (rf result (conj sg item)))
                 (seq sg)
                 (do (vswap! subgroup conj item)
                     result)
                 (start? item)
                 (do (vreset! subgroup [item])
                     result)
                 :else (rf result item))))))))
    

    (into [] (subgroups-all #{:b} #{:d}) [:a :b :c :d :e :f :g :h :b :x])
    => [:a [:b :c :d] :e :f :g :h [:b :x]]
    (into [] (subgroups-all #{:b} #{:d}) [:a :b :c :e :f :g :h :b :x])
    => [:a [:b :c :e :f :g :h :b :x]]
    

    请注意,在上一个示例中,嵌套的开始/打开不会导致嵌套分组,这让我想到了另一个解决方案。。。

    嵌套组和拉链

    当我把这更一般地看作是“打开”序列时,我想到了拉链:

    (defn unflatten [open? close? coll]
      (when (seq coll)
        (z/root
         (reduce
          (fn [loc elem]
            (cond
              (open? elem)
              (-> loc (z/append-child (list elem)) z/down z/rightmost)
              (and (close? elem) (z/up loc))
              (-> loc (z/append-child elem) z/up)
              :else (z/append-child loc elem)))
          (z/seq-zip ())
          coll))))
    

    这将在空列表上创建一个拉链,并使用 reduce 在输入序列上。它使用一对谓词来打开/关闭组,并允许任意嵌套组:

    (unflatten #{:b} #{:d} [:a :b :c :b :d :d :e :f])
    => (:a (:b :c (:b :d) :d) :e :f)
    (unflatten #{:b} #{:d} [:a :b :c :b :d :b :b :d :e :f])
    => (:a (:b :c (:b :d) (:b (:b :d) :e :f)))
    (unflatten #{:b} #{:d} [:b :c :e :f])
    => ((:b :c :e :f))
    (unflatten #{:b} #{:d} [:d :c :e :f])
    => (:d :c :e :f)
    (unflatten #{:b} #{:d} [:c :d])
    => (:c :d)
    (unflatten #{:b} #{:d} [:c :d :b])
    => (:c :d (:b))
    
        3
  •  2
  •   Rulle    7 年前

    clojure.spec 为了这个。首先,为要分析的数据定义语法:

    (ns playground.startstop
      (:require [clojure.spec.alpha :as spec]))
    
    (defn start? [x] (= x :b))
    (defn stop?  [x] (= x :d))
    
    (spec/def ::not-start-stop #(and (not (start? %))
                                     (not (stop? %))))
    
    (spec/def ::group (spec/cat :start start?
                                :contents (spec/* ::not-start-stop)
                                :stop stop?))
    
    (spec/def ::element (spec/alt :group ::group
                                  :primitive ::not-start-stop))
    
    (spec/def ::elements (spec/* ::element))
    

    现在您可以使用 conform 函数来解析数据:

    (def data [:a :b :c :d :e :f :g :h :b :d :x])
    
    (spec/conform ::elements data)
    ;; => [[:primitive :a] [:group {:start :b, :contents [:c], :stop :d}] [:primitive :e] [:primitive :f] [:primitive :g] [:primitive :h] [:group {:start :b, :stop :d}] [:primitive :x]]
    

    上面的输出不是我们想要的,因此我们定义函数来呈现结果:

    (defn render [[type data]]
      (case type
        :primitive data
        :group `[~(:start data) ~@(:contents data) ~(:stop data)]))
    

    并将其映射到已解析的数据上:

    (mapv render (spec/conform ::elements data))
    ;; => [:a [:b :c :d] :e :f :g :h [:b :d] :x]
    

        4
  •  1
  •   Alan Thompson    7 年前

    split-with 可以用来做大部分的工作。唯一棘手的一点是使子组包含 stop? 还有价值。这里有一个解决方案:

    (ns tst.demo.core
      (:use tupelo.core demo.core tupelo.test))
    
    (def data [:a :b :c :d :e :f :g :h :b :d :x])
    
    (defn start? [x] (= x :b))
    (defn stop?  [x] (= x :d))
    
    (defn parse [vals]
      (loop [result []
             vals   vals]
        (if (empty? vals)
          result
          (let [[singles group-plus]  (split-with #(not (start? %)) vals)
                [grp* others*]        (split-with #(not (stop? %)) group-plus)
                grp        (glue grp* (take 1 others*))
                others     (drop 1 others*)
                result-out (cond-it-> (glue result singles)
                             (not-empty? grp) (append it grp))]
            (recur result-out others)))))
    

    结果:

    (parse data) => [:a [:b :c :d] :e :f :g :h [:b :d] :x]
    

    t/glue t/append 所以我们可以 always deal with vectors and append only at the end (不是一开始 conj 不使用列表)。


    使现代化

    使用 cond-it-> 在最后,以避免粘在一个空的 [] trampoline 功能:

    (ns tst.demo.core
      (:use tupelo.core demo.core tupelo.test))
    
    (def data [:a :b :c :d :e :f :g :h :b :d :x])
    
    (defn start? [x] (= x :b))
    (defn stop?  [x] (= x :d))
    
    (declare parse-singles parse-group)
    
    (defn parse-singles [result vals]
      (if (empty? vals)
        result
        (let [[singles groupies] (split-with #(not (start? %)) vals)
              result-out (glue result singles)]
          #(parse-group result-out groupies))))
    
    (defn parse-group [result vals]
      (if (empty? vals)
        result
        (let [[grp-1 remaining] (split-with #(not (stop? %)) vals)
              grp      (glue grp-1 (take 1 remaining))
              singlies (drop 1 remaining)
              result-out   (append result grp)]
          #(parse-singles result-out singlies))))
    
    (defn parse [vals]
      (trampoline parse-singles [] vals))
    
    (dotest
      (spyx (parse data)))
    
    (parse data) => [:a [:b :c :d] :e :f :g :h [:b :d] :x]
    

    parse-singles parse-group 你真的不需要使用 蹦床 . 在这种情况下,只需卸下 # 解析单子 解析组 蹦床 parse .


    Clojure备忘单

    和往常一样,别忘了 bookmark the Clojure CheatSheet!

        5
  •  1
  •   user499049    7 年前

    这是一个使用lazy seq和split with的版本。 关键是考虑序列中每个元素需要生成什么,在这种情况下,伪代码如下所示:

    ;; for each element (e) in the input sequence
    
    if (start? e) 
      (produce values up to an including (stop? e))
    else 
      e
    

    (def data [:a :b :c :d :e :f :g :h :b :d :x])
    
    (def start? #(= :b %))
    (def stop?  #(= :d %))
    
    (defn parse [vals]
      (when-let [e (first vals)]
        (let [[val rst] (if (start? e)
                          (let [[run remainder] (split-with (complement stop?) vals)]
                            [(concat run [(first remainder)]) (rest remainder)])
                          [e (rest vals)])]
          (cons val (lazy-seq (parse rst))))))
    
    ;; this produces the following output
    (parse data) ;; => (:a (:b :c :d) :e :f :g :h (:b :d) :x)
    
        6
  •  1
  •   fl00r    7 年前
    (defn start? [x] (= x :b))
    (defn stop?  [x] (= x :d))
    (def data [:a :b :c :d :e :f :g :h :b :d :c])
    

    看起来像 split-with 应该是个不错的选择,但是我

    (loop [data data
           res []]
      (let [[left tail] (split-with (comp not start?) data)
            [group [stop & new-data]] (split-with (comp not stop?) tail)
            group (cond-> (vec group) stop (into [stop]))
            new-res (cond-> (into res left)
                      (seq group) (into [group]))]
        (if (seq new-data)
          (recur new-data new-res)
          new-res)))
    
        7
  •  1
  •   akond    7 年前

    只是因为我喜欢FSM和快板凳。

    (let [start? #(= % :b)
              stop?  #(= % :d)
              data   [:a :b :c :d :e :f :g :h :b :d :x]]
            (letfn [(start [result [x & xs]]
                        #(collect-vec (conj result [x]) xs))
    
                    (collect-vec [result [x & xs]]
                        #(if (nil? x)
                             result
                             ((if (stop? x) initial collect-vec)
                                 (conj (subvec result 0 (dec (count result))) (conj (last result) x)) xs)))
    
                    (collect [result [x & xs]]
                        #(initial (conj result x) xs))
    
                    (initial [result [x & xs :as v]]
                        (cond (nil? x) result
                              (start? x) #(start result v)
                              :else (fn [] (collect result v))))]
                (trampoline initial [] data)))
    
    推荐文章