Clojure

轉換器

轉換器是可以組合的演算法轉換。它們獨立於其輸入和輸出來源的背景,並且僅根據單獨元素指定轉換的本質。由於轉換器與輸入或輸出來源分離,因此它們可以用於許多不同的程序 - 集合、串流、通道、可觀察對象等。轉換器直接組合,而不會意識到輸入或建立中間聚合。

另請參閱介紹性的 部落格文章、這個 影片,以及常見問題解答的這一部分 關於轉換器的良好使用案例

術語

減少函數是您傳遞給 reduce 的函數類型 - 它是一個函數,它接受累積結果和新的輸入,並傳回新的累積結果

;; reducing function signature
whatever, input -> whatever

轉換器(有時稱為 xform 或 xf)是從一個減少函數到另一個減少函數的轉換

;; transducer signature
(whatever, input -> whatever) -> (whatever, input -> whatever)

使用轉換器定義轉換

Clojure 中包含的大多數序列函數都具有產生轉換器的元數。此元數省略輸入集合;輸入將由套用轉換器的程序提供。注意:此減少的元數不是柯里化或部分應用。

例如

(filter odd?) ;; returns a transducer that filters odd
(map inc)     ;; returns a mapping transducer for incrementing
(take 5)      ;; returns a transducer that will take the first 5 values

轉換器與一般函數組合組合。轉換器在決定是否以及呼叫它封裝的轉換器多少次之前執行其操作。建議使用現有的 comp 函數組合轉換器

(def xf
  (comp
    (filter odd?)
    (map inc)
    (take 5)))

轉換器 xf 是轉換堆疊,程序會將其應用於一系列輸入元素。堆疊中的每個函數在它封裝的操作之前執行。轉換器的組合從右到左執行,但會建立從左到右執行的轉換堆疊(在此範例中,篩選發生在對應之前)。

作為助記符,請記住 comp 中轉換器函數的順序與 ->> 中序列轉換的順序相同。以上的轉換等於序列轉換

(->> coll
     (filter odd?)
     (map inc)
     (take 5))

使用轉換器

轉換器可以在許多情況下使用(以下說明如何建立新的轉換器)。

transduce

應用轉換器最常見的方法之一是使用 transduce 函數,它類似於標準的 reduce 函數

(transduce xform f coll)
(transduce xform f init coll)

transduce 會立即(非延遲)使用應用於還原函數 f 的轉換器 xform 還原 coll,如果提供 init 則使用 init 作為初始值,否則使用 (f)。f 提供如何累積結果的知識,這發生在還原的(潛在有狀態的)環境中。

(def xf (comp (filter odd?) (map inc)))
(transduce xf + (range 5))
;; => 6
(transduce xf + 100 (range 5))
;; => 106

組合的 xf 轉換器將從左到右呼叫,最後呼叫還原函數 f。在最後一個範例中,輸入值會先過濾,然後遞增,最後求和。

Nested transformations

eduction

若要擷取將轉換器應用於 coll 的程序,請使用 eduction 函數。它採用任意數量的 xform 和最終 coll,並傳回轉換器對 coll 中項目的可還原/可迭代應用。每次呼叫 reduce/iterator 時,都會執行這些應用。

(def iter (eduction xf (range 5)))
(reduce + 0 iter)
;; => 6

into

若要將轉換器應用於輸入集合並建構新的輸出集合,請使用 into(如果可能,它會有效率地使用 reduce 和 transients)

(into [] xf (range 1000))

序列

若要從換能器的應用程式建立輸入集合的序列,請使用 sequence

(sequence xf (range 1000))

結果序列元素會逐步計算。這些序列會視需要逐步使用輸入,並完全實現中介運算。此行為與惰性序列上的等效運算不同。

建立換能器

換能器的形狀如下(自訂程式碼為「…​」)

(fn [rf]
  (fn ([] ...)
      ([result] ...)
      ([result input] ...)))

許多核心序列函數(例如 map、filter 等)會採用特定於運算的引數(謂詞、函數、計數等),並傳回封閉在這些引數中的此形狀的換能器。在某些情況下,例如 cat,核心函數就是換能器函數,且不會採用 rf

內部函數定義有 3 個用於不同目的的 arity

  • Init(arity 0) - 應呼叫巢狀轉換 rf 上的 init arity,最終會呼叫出換能器處理程序。

  • Step(arity 2) - 這是標準的簡約函數,但預期會在換能器中適當地呼叫 rf step arity 0 次或更多次。例如,filter 會根據謂詞選擇是否呼叫 rf。map 會永遠只呼叫一次。cat 會根據輸入呼叫多次。

  • Completion(arity 1) - 有些處理程序不會結束,但對於那些會結束的處理程序(例如 transduce),completion arity 會用於產生最終值和/或清除狀態。此 arity 必須呼叫 rf completion arity 一次。

completion 的範例用途為 partition-all,它必須在輸入結束時清除所有剩餘元素。 completing 函數可用於透過新增預設完成 arity 來將簡約函數轉換為換能器函數。

提早終止

Clojure 有指定簡約提早終止的機制

  • reduced - 採用一個值並傳回簡約值,表示簡約應停止

  • reduced? - 如果值是用 reduced 建立的,則傳回 true

  • deref 或 @ 可用於擷取 reduced 內的值

使用換能器的處理程序必須檢查並在 step 函數傳回簡約值時停止(在建立可換能處理程序中會詳細說明)。此外,使用巢狀簡約的換能器 step 函數必須在遇到簡約值時檢查並傳達簡約值。(請參閱 cat 的實作以取得範例。)

具有簡約狀態的換能器

某些轉換器(例如 takepartition-all 等)在還原過程中需要狀態。每次可轉換的過程套用轉換器時,都會建立此狀態。例如,考慮將一系列重複值壓縮成單一值的 dedupe 轉換器。此轉換器必須記住前一個值,以確定是否應傳遞目前的數值

(defn dedupe []
  (fn [xf]
    (let [prev (volatile! ::none)]
      (fn
        ([] (xf))
        ([result] (xf result))
        ([result input]
          (let [prior @prev]
            (vreset! prev input)
              (if (= prior input)
                result
                (xf result input))))))))

在 dedupe 中,prev 是在還原期間儲存前一個值的狀態容器。prev 值是為了效能而設的暫存變數,但它也可以是原子。prev 值會在轉換過程開始之前初始化(例如在呼叫 transduce 時)。因此,狀態互動會包含在可轉換過程的內容中。

在完成步驟中,具有還原狀態的轉換器應在呼叫嵌套轉換器的完成函式之前清除狀態,除非它先前已從嵌套步驟中看到已還原的值,否則應捨棄待處理的狀態。

建立可轉換的過程

轉換器設計用於在許多類型的過程中使用。可轉換的過程定義為一系列步驟,每個步驟都會擷取輸入。輸入的來源會因每個過程而異(從集合、反覆運算器、串流等)。同樣地,此過程必須選擇如何處理每個步驟產生的輸出。

如果您有套用轉換器的全新內容,則有幾個一般規則需要注意

  • 如果步驟函式傳回已還原的值,則可轉換的過程不得再提供任何輸入給步驟函式。已還原的值必須在完成之前以 deref 解開。

  • 完成的過程必須在最後累積的值上呼叫完成操作,且只能呼叫一次。

  • 轉換的過程必須封裝對呼叫轉換器所傳回函式的參照 - 這些函式可能是狀態的,且不適合跨執行緒使用。