繁体   English   中英

如何运行在同一个集合上运行的多个函数,但只遍历集合一次? (clojure,包括示例)

[英]How do I run multiple functions which operate on the same collection, but only traverse the collection once? (clojure, example included)

有点奇怪,但我基本上需要在一个向量上运行两个独立的函数。 他们都在向量上 map,并返回一个结果。 如果我要一个接一个地运行它们,这意味着要检查两次集合 - 我将如何做到这一点,以便我只需要 map 一次,并且可以执行这两个功能? 功能本身无法更改,因为它们在其他地方独立使用。 我可能没有多大意义,所以举个例子可能会更好:

(defn add-one [xs] (map #(+ % 1) xs))
(defn minus-one [xs] (map #(- % 1) xs))
(def my-xs [1 2 3])
(def result {:added (add-one my-xs) :minused (minus-one my-xs)})

所以,我基本上希望能够计算“结果”,但只需要 go 超过“xs”一次。 鉴于功能需要一个集合,我不确定是否有可能做到这一点,但我想我会检查以防我缺少一些 clojure 魔法:D

编辑 - 在这个例子中我可以只使用 inc/dec,但关键是我需要利用对集合进行操作的函数,因为实际的函数要复杂得多:)

没有通用的解决方案。 如果您有两个任意函数消耗一个序列并以某种未知方式对其进行操作以产生结果,则无法避免遍历该序列两次。

对于功能上的各种约束,组合是可能的。 您已经在评论中看到[(map f xs) (map g xs)]如何替换为(apply map list (map (juxt fg) xs)) 对于具有幺半群结构的消费者,可以做类似的事情,比如结合 min 和 max,或者如果它们都只是(fn [xs] (reduce fa xs))

这个想法是采用集合函数add-oneminus-one ,并使它们适用于单个元素:

(defn singlify [fun] (fn [x] (first (fun [x]))))

;; now you can do:
((singlify #'add-one) 3)
;; => 4
;; so #'add-one became a function applicable for one element only
;; instead to an entire sequence/collection
;; - well actually we just wrap around the argument x a vector and apply
;; the sequence-function on this `[x]` and then take the result out of the
;; result list/vec/seq by a `first` - so this is not the most performant solution.
;; however, we can now use the functions and get the single-element versions
;; of them without having to modify the original functions.
;; And this solution is generalize-able to all collection functions.

现在使用评论的有用提示,它juxt我们可以在一个序列上应用两个不同的函数,同时只遍历它一次,我们得到

(map (juxt (singlify #'add-one) (singlify #'minus-one)) my-xs)
;; => ([2 0] [3 1] [4 2])

使用zipmap和有用的 lispy 成语(apply map #'<collector-function> <input-collection>)来转置结果列表,我们可以将它们拆分为预先带有相应关键字的 dict/map:

(zipmap [:added :minused] 
        (apply map vector 
               (map (juxt (singlify #'add-one) 
                          (singlify #'minus-one)) 
                    my-xs)))
;; => {:added [2 3 4], :minused [0 1 2]}

概括为 function

我们可以将其概括为 function ,它接受一个键序列、一个待应用集合函数的序列和一个只遍历一次的输入序列/集合:

;; this helper function applies `juxt` 
;; on the `singlify`-ed versions of the collection-functions:
(defn juxtify [funcs] (apply #'juxt (map #(singlify %) funcs)))

;; so the generalized function is:
(defn traverse-once [keys seq-funcs sq]
  (zipmap keys (apply map vector (map (juxtify seq-funcs) sq))))

使用此 function,示例案例如下所示:

(traverse-once [:added :minused] [#'add-one #'minus-one] my-xs)
;; => {:added [2 3 4], :minused [0 1 2]}

我们现在可以随心所欲地扩展:

(traverse-once [:squared 
                :minused 
                :added] 
               [(fn [sq] (map #(* % %) sq)) 
                #'minus-one 
                #'add-one] 
               my-xs)
;; => {:squared [1 4 9], :minused [0 1 2], :added [2 3 4]}

瞧!

功能本身无法更改,因为它们在其他地方独立使用。

重构,使收集值的操作独立于序列消耗,然后在这些重构之上重新实现序列消耗函数(因此尊重现有的 API,因为这是一个硬约束),然后根据需要组合独立的操作以避免重复序列迭代。

如果您从组织外部使用上游 package,请打开一个关于他们更改 API 以允许以这种方式高效运行的对话框。

任何围绕诸如 singlify 之类的函数进行挖掘的方法都可能会以微妙的方式破坏或使未来的维护者感到困惑,即使假设将序列项装箱以供重新使用的成本不是性能问题。

  • 遍历序列一次,使用juxt计算每个元素所需的所有结果 - 将其称为base序列。
  • 返回序列的 map,每个序列从base序列的元素中选择自己的元素。 这是按需转置。

因此:

(defn one-pass-maps [fn-map]
  (let [fn-vector (apply juxt (vals fn-map))]
    (fn [coll]
      (let [base (map fn-vector coll)]
        (zipmap (keys fn-map) (map (fn [n] (map #(% n) base)) (range)))))))

例如,

((one-pass-maps {:inc inc, :dec dec}) (range 10))
=> {:inc (1 2 3 4 5 6 7 8 9 10), :dec (-1 0 1 2 3 4 5 6 7 8)}

所有的遍历都是惰性的。 base序列仅在任何转置序列行进时才被实现。 但是,如果一个序列被实现到——比如说——第五个元素,它们都是。

一个有用的通用策略是以代数风格表达 function 并使用各种代数定律对其进行优化。 在这个答案中,我将重点关注可以通过reducetransduce表达序列处理的情况,这可能捕捉到急切遍历序列的概念,并使用一些“元组法则”。 为了简洁明了,我将省略提前终止的处理( reduced ),这是一个不太难添加的功能。

首先,我将使用如下所示的reducetransduce的修改版本,它们对于当前情况具有一些更理想的属性:

(defn reduce
  ([f coll] (reduce f (f) coll))
  ([f init coll] (clojure.core/reduce f init coll)))

(defn transduce [xf f & args]
  (let [f (xf f)]
    (f (apply reduce f args))))

我还将介绍juxtmapmap-indexed的多态版本,它们对向量和地图进行操作:

(defprotocol JuxtMap
  (juxt* [fs])
  (map* [coll f])
  (map-indexed* [coll f]))

(extend-protocol JuxtMap

  clojure.lang.IPersistentVector
  (juxt* [fs]
    (fn [& xs]
      (into [] (map #(apply % xs)) fs)))
  (map* [coll f]
    (into [] (map f) coll))
  (map-indexed* [coll f]
    (into [] (map-indexed f) coll))

  clojure.lang.IPersistentMap
  (juxt* [fs]
    (fn [& xs]
      (into {} (map (juxt key (comp #(apply % xs) val))) fs)))
  (map* [coll f]
    (into {} (map (juxt key (comp f val))) coll))
  (map-indexed* [coll f]
    (into {} (map (juxt key (partial apply f))) coll)))

(letfn [(flip [f] #(f %2 %1))]
  (def map* (flip map*))
  (def map-indexed* (flip map-indexed*)))

现在我们可以创建两个函数, juxt*-rfjuxt*-xf ,它们满足以下“元组规则”,用o表示组合并假设reducetransducemap*在它们的第一个参数中被柯里化,而前两个不'没有收到init

  • reduce o juxt*-rf = juxt* o map*(reduce)
  • transduce o juxt*-xf = juxt* o map*(transduce)

他们来了:

(def juxt*-rf
  (comp
    juxt*
    (partial map-indexed*
      (fn [i f]
        (fn
          ([] (f))
          ([acc] (f (acc i)))
          ([acc x] (f (acc i) x)))))))

(def juxt*-xf
  (comp
    (partial comp juxt*-rf)
    juxt*))

最后,让我们看看juxt*-xf的作用:

(defn average [coll]
  (->> coll
       (transduce
         (juxt*-xf [identity (map (constantly 1))])
         +)
       (apply /)))

(average [1 2 3])
;result: 2

(defn map-many [fs coll]
  (transduce
    (juxt*-xf (map* map fs))
    conj
    coll))

(map-many {:inc inc, :dec dec} [1 2 3])
;result: {:inc [2 3 4], :dec [0 1 2]}

(transduce
  (juxt*-xf {:transp (juxt*-xf (map* map [first second]))
             :concat cat})
  conj
  [[1 11] [2 12] [3 13]])
;result: {:transp [[1 2 3] [11 12 13]], :concat [1 11 2 12 3 13]}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM