簡體   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