有人可以用简单的术语向我解释Clojure Transducers吗?

时间:2014-10-11 17:31:05

标签: clojure transducer

我已经尝试过阅读,但我仍然不了解它们的价值或它们的替代品。他们是否让我的代码更短,更容易理解?或者是什么?

更新

很多人发布了答案,但很高兴看到有或没有换能器的例子非常简单,甚至像我这样的白痴也能理解。当然,除非传感器需要一定程度的理解,否则我永远不会理解它们:(

12 个答案:

答案 0 :(得分:70)

传感器是如何处理数据序列的方法,而不知道底层序列是什么(如何操作)。它可以是任何seq,异步通道或可观察的。

它们是可组合的和多态的。

好处是,每次添加新数据源时都不必实现所有标准组合器。一次又一次。由此产生的效果,您作为用户可以在不同的数据源上重用这些配方。

广告更新

Clojure的1.7版之前,您有三种编写数据流查询的方法:

  1. 嵌套电话
  2.     (reduce + (filter odd? (map #(+ 2 %) (range 0 10))))
    
    1. 功能组合
    2.     (def xform
            (comp
              (partial filter odd?)
              (partial map #(+ 2 %))))
          (reduce + (xform (range 0 10)))
      
      1. 线程宏
      2.     (defn xform [xs]
              (->> xs
                   (map #(+ 2 %))
                   (filter odd?)))
            (reduce + (xform (range 0 10)))
        

        使用传感器,您可以像:

        (def xform
          (comp
            (map #(+ 2 %))
            (filter odd?)))
        (transduce xform + (range 0 10))
        

        他们都这样做。不同之处在于你从不直接调用传感器,而是将它们传递给另一个函数。传感器知道该做什么,让传感器知道如何做的功能。组合器的顺序就像你用线程宏(自然顺序)编写它。现在,您可以将xform重复使用频道:

        (chan 1 xform)
        

答案 1 :(得分:40)

传感器可提高效率,并允许您以更模块化的方式编写高效代码。

This is a decent run through

与撰写对旧mapfilterreduce等的调用相比,您可以获得更好的效果,因为您不需要在每个步骤之间构建中间集合,反复走这些收藏品。

reducers相比,或者将所有操作手动组合成单个表达式,您可以更轻松地使用抽象,更好的模块化和处理函数的重用。

答案 2 :(得分:20)

传感器是减少功能的组合手段。

实施例: 减少函数是带有两个参数的函数:到目前为止的结果和输入。他们返回一个新的结果(到目前为止)。例如+:使用两个参数,您可以将第一个视为结果,第二个视为输入。

传感器现在可以使用+功能并使其成为两倍以上的功能(在添加之前将每个输入加倍)。这就是换能器的样子(在大多数基本术语中):

(defn double
  [rfn]
  (fn [r i] 
    (rfn r (* 2 i))))

为了说明,请rfn替换+以查看+如何转换为两倍以上:

(def twice-plus ;; result of (double +)
  (fn [r i] 
    (+ r (* 2 i))))

(twice-plus 1 2)  ;-> 5
(= (twice-plus 1 2) ((double +) 1 2)) ;-> true

所以

(reduce (double +) 0 [1 2 3]) 

现在会产生12。

换能器返回的减少函数与结果的累积无关,因为它们会随着传递给它们的递减函数而累积,不知道如何。在这里,我们使用conj代替+Conj获取一个集合和一个值,并返回一个附加了该值的新集合。

(reduce (double conj) [] [1 2 3]) 

会产生[2 4 6]

它们也与输入的来源无关。

多个传感器可以链接为(可链接)配方,以转换还原功能。

更新:由于现在有关于它的官方网页,我强烈建议您阅读:http://clojure.org/transducers

答案 3 :(得分:19)

假设您想使用一系列函数来转换数据流。 Unix shell允许您使用管道运算符执行此类操作,例如

cat /etc/passwd | tr '[:lower:]' '[:upper:]' | cut -d: -f1| grep R| wc -l

(以上命令计算用户名中带有大写或小写字母r的用户数)。这是作为一组进程实现的,每个进程都从先前进程的输出中读取,因此有四个中间流。您可以想象一个不同的实现,它将五个命令组合成一个聚合命令,该命令将从其输入读取并将其输出恰好写入一次。如果中间流很昂贵,而且成分便宜,那可能是一个很好的权衡。

同样的事情适用于Clojure。有多种方法可以表达转换管道,但是根据您的操作方式,最终可以得到从一个函数传递到下一个函数的中间流。如果您有大量数据,那么将这些功能组合成单个功能会更快。传感器可以很容易地做到这一点。早期的Clojure创新,减速器,让你也这样做,但有一些限制。传感器消除了一些限制。

因此,为了回答您的问题,传感器不一定能使您的代码更短或更容易理解,但您的代码可能不会更长或更不易理解,如果您正在使用大量数据,传感器可以使您的代码更快。

This是传感器的一个很好的概述。

答案 4 :(得分:10)

Rich Hickey在Strange Loop 2014大会(45分钟)上发表了“Transducers”演讲。

他用简单的方式解释了传感器是什么,用现实世界的例子 - 在机场处理行李。他清楚地区分了不同的方面,并将它们与当前的方法进行了对比。最后,他给出了他们存在的理由。

视频:https://www.youtube.com/watch?v=6mTbuzafcII

答案 5 :(得分:8)

我发现阅读transducers-js中的示例帮助我以具体的方式理解它们如何在日常代码中使用它们。

例如,考虑这个例子(取自上面链接的README):

var t = require("transducers-js");

var map    = t.map,
    filter = t.filter,
    comp   = t.comp,
    into   = t.into;

var inc    = function(n) { return n + 1; };
var isEven = function(n) { return n % 2 == 0; };
var xf     = comp(map(inc), filter(isEven));

console.log(into([], xf, [0,1,2,3,4])); // [2,4]

首先,使用xf看起来比使用Underscore的通常替代方案更清晰。

_.filter(_.map([0, 1, 2, 3, 4], inc), isEven);

答案 6 :(得分:7)

传感器(据我理解!)函数采用一个 reduce 函数并返回另一个函数。

是一种还原功能

例如:

user> (def my-transducer (comp count filter))
#'user/my-transducer
user> (my-transducer even? [0 1 2 3 4 5 6])
4
user> (my-transducer #(< 3 %) [0 1 2 3 4 5 6])
3

在这种情况下,my-transducer采用输入过滤功能,如果该值为偶数则应用于0?在第一种情况下,过滤器将该值传递给计数器,然后它过滤下一个值。而不是先过滤然后将所有这些值传递给count。

在第二个例子中,它一次检查一个值,如果该值小于3,那么它会让count加1。

答案 7 :(得分:7)

传感器清晰定义如下:

Transducers are a powerful and composable way to build algorithmic transformations that you can reuse in many contexts, and they’re coming to Clojure core and core.async.

要理解它,让我们考虑以下简单示例:

;; The Families in the Village

(def village
  [{:home :north :family "smith" :name "sue" :age 37 :sex :f :role :parent}
   {:home :north :family "smith" :name "stan" :age 35 :sex :m :role :parent}
   {:home :north :family "smith" :name "simon" :age 7 :sex :m :role :child}
   {:home :north :family "smith" :name "sadie" :age 5 :sex :f :role :child}

   {:home :south :family "jones" :name "jill" :age 45 :sex :f :role :parent}
   {:home :south :family "jones" :name "jeff" :age 45 :sex :m :role :parent}
   {:home :south :family "jones" :name "jackie" :age 19 :sex :f :role :child}
   {:home :south :family "jones" :name "jason" :age 16 :sex :f :role :child}
   {:home :south :family "jones" :name "june" :age 14 :sex :f :role :child}

   {:home :west :family "brown" :name "billie" :age 55 :sex :f :role :parent}
   {:home :west :family "brown" :name "brian" :age 23 :sex :m :role :child}
   {:home :west :family "brown" :name "bettie" :age 29 :sex :f :role :child}

   {:home :east :family "williams" :name "walter" :age 23 :sex :m :role :parent}
   {:home :east :family "williams" :name "wanda" :age 3 :sex :f :role :child}])

我们想知道村里有多少孩子?我们可以使用以下减速器轻松找到它:

;; Example 1a - using a reducer to add up all the mapped values

(def ex1a-map-children-to-value-1 (r/map #(if (= :child (:role %)) 1 0)))

(r/reduce + 0 (ex1a-map-children-to-value-1 village))
;;=>
8

这是另一种方法:

;; Example 1b - using a transducer to add up all the mapped values

;; create the transducers using the new arity for map that
;; takes just the function, no collection

(def ex1b-map-children-to-value-1 (map #(if (= :child (:role %)) 1 0)))

;; now use transduce (c.f r/reduce) with the transducer to get the answer 
(transduce ex1b-map-children-to-value-1 + 0 village)
;;=>
8

此外,在使用帐户中的子组时它也非常强大。例如,如果我们想知道Brown Family中有多少孩子,我们可以执行:

;; Example 2a - using a reducer to count the children in the Brown family

;; create the reducer to select members of the Brown family
(def ex2a-select-brown-family (r/filter #(= "brown" (string/lower-case (:family %)))))

;; compose a composite function to select the Brown family and map children to 1
(def ex2a-count-brown-family-children (comp ex1a-map-children-to-value-1 ex2a-select-brown-family))

;; reduce to add up all the Brown children
(r/reduce + 0 (ex2a-count-brown-family-children village))
;;=>
2

我希望你能找到有用的这些例子。您可以找到更多here

希望它有所帮助。

Clemencio Morales Lucas。

答案 8 :(得分:4)

我用clojurescript example在博客上写了这篇文章,它解释了序列函数现在是如何通过能够替换reduce函数来扩展的。

这是我读过的传感器的要点。如果您考虑在consconj等操作中硬编码的mapfilter操作,则无法访问还原功能。

使用换能器,还原功能被解耦,我可以像使用原生javascript数组push一样替换它,这要归功于换能器。

(transduce (filter #(not (.hasOwnProperty prevChildMapping %))) (.-push #js[]) #js [] nextKeys)

filter和朋友们进行了新的1次操作,它将返回一个转换功能,您可以使用它来提供自己的缩减功能。

答案 9 :(得分:3)

这是我的(大多数)行话和无代码答案。

以两种方式考虑数据:流(随时间发生的值,如事件)或结构(存在于某个时间点的数据,如列表,向量,数组等)。

您可能希望对流或结构执行某些操作。一种这样的操作是映射。映射函数可能会将每个数据项(假设它是一个数字)递增1,您可以想象它如何应用于流或结构。

映射函数只是一类函数中的一种,有时称为&#34;减少函数&#34;。另一个常见的缩减函数是过滤器,它删除与谓词匹配的值(例如,删除所有偶数值)。

传感器让你&#34;包裹&#34;一系列一个或多个缩减函数并生成一个&#34;包&#34; (它本身就是一个函数),适用于流或结构。例如,您可以&#34;打包&#34;一系列减少函数(例如,过滤偶数,然后映射结果数字以将它们递增1)然后使用该传感器&#34;包&#34;在流或值的结构(或两者)上。

那么这有什么特别之处呢?通常,减少功能无法有效地组合以在流和结构上工作。

因此,您可以利用围绕这些功能的知识并将其应用于更多用例。您需要花费一些额外的机器(即换能器)来为您提供额外的动力。

答案 10 :(得分:2)

据我了解,它们就像构建块,与输入和输出实现分离。您只需定义操作。

由于操作的实现不在输入代码中,并且输出没有任何操作,因此传感器非常可重复使用。它们让我想起 Akka Streams 中的 Flow

我也是换能器的新手,对于可能不清楚的答案感到抱歉。

答案 11 :(得分:1)

我发现这篇文章让你对换能器有了更多的鸟瞰图。

https://medium.com/@roman01la/understanding-transducers-in-javascript-3500d3bd9624