有人可以用简单的方式向我解释Clojure换能器吗?


100

我曾尝试阅读此书,但我仍然不了解它们的价值或它们所替代的价值。它们会使我的代码更短,更易于理解吗?

更新资料

很多人都发布了答案,但是很高兴看到带有或不带有换能器的示例,它们非常简单,即使像我这样的白痴也可以理解。除非换能器当然需要一定程度的了解,否则我将永远不会理解它们:(

Answers:


75

换能器是在不知道底层序列是什么的情况下如何处理数据序列的方法。它可以是任何seq,异步通道,也可以是可观察的。

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

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

广告更新

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

  1. 嵌套通话
    (reduce + (filter odd? (map #(+ 2 %) (range 0 10))))
  1. 功能组成
    (def xform
      (comp
        (partial filter odd?)
        (partial map #(+ 2 %))))
    (reduce + (xform (range 0 10)))
  1. 线程宏
    (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)

3
我一直在寻找一个示例附带的答案,该示例向我展示了传感器如何为我节省时间。
appshare.co

如果您不是Clojure或某些数据流库维护者,则不会。
2014年

5
这不是技术决定。我们仅使用基于业务价值的决策。“只使用它们”会让我被解雇
appshare.co 2014年

1
如果您延迟尝试使用换能器,直到Clojure 1.7发布,您可能会更轻松地保留工作。
user100464 2014年

7
换能器似乎是抽象各种形式的可迭代对象的有用方法。这些可以是非消耗性的(例如Clojure seqs),也可以是消耗性的(例如异步通道)。在这方面,在我看来,例如,如果您使用通道从基于seq的实施方式切换到core.async实施方式,则将从换能器中受益匪浅。换能器应允许您保持逻辑核心不变。使用传统的基于序列的处理,您必须将其转换为使用换能器或某些内核异步模拟。那是商业案例。
内森·戴维斯

47

换能器提高了效率,并允许您以更加模块化的方式编写高效的代码。

这是一个不错的过程

相比构成调用老mapfilterreduce等你获得更好的性能,因为你不需要建立中间集合每个步骤之间,反反复复走的集合。

与相比reducers,或将所有操作手动组合为一个表达式,您将更易于使用抽象,更好的模块化和重用处理功能。


2
您只是想知道,您上面说过:“在每个步骤之间建立中间集合”。但是“中间集合”听起来不像是反模式吗?.NET提供了惰性枚举,Java提供了惰性流或Guava驱动的可迭代,惰性Haskell也必须具有惰性。这些都不要求map/ reduce使用中间集合,因为它们都构建了迭代器链。我在哪里错了?
Lyubomyr Shaydariv 2014年

3
Clojure mapfilter在嵌套时创建中间集合。
Noisesmith

4
至少就Clojure的懒惰版本而言,懒惰问题在这里是正交的。是的,映射和过滤器是惰性的,当您链接它们时,它们还会为延迟值生成容器。如果您不坚持到底,就不会建立不需要的大型惰性序列,但是您仍然会为每个惰性元素构建这些中间抽象。
noisesmith 2014年

一个例子会很好。
appshare.co 2014年

8
@LyubomyrShaydariv通过“中间集合”,noisesmith的意思不是“迭代/确定整个集合,然后迭代/确定另一个整个集合”。他或她的意思是,当您嵌套返回顺序的函数调用时,每个函数调用都会导致创建一个新的顺序。实际的迭代仍然只发生一次,但是由于嵌套的顺序,因此存在额外的内存消耗和对象分配。
erikprice 2014年

22

换能器是减少功能的组合方式。

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

换能器现在可以采用+功能,使其成为两次加法功能(将每个输入加倍,然后再添加)。这就是换能器的外观(从最基本的角度来看):

(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


很好的解释,但很快对我来说就成了太多的术语,“减少传感器产生的功能与结果的累积方式无关”。
appshare.co 2014年

1
您是对的,这里产生的单词不合适。
Leon Grapenthin 2014年

没关系。无论如何,我了解到《变形金刚》现在只是一种优化,因此无论如何都不应该使用
appshare.co 2014年

1
它们是减少功能的组合方式。你在哪里还有那?这远远超过了优化。
Leon Grapenthin 2014年

我发现这个答案非常有趣,但是我不清楚它如何连接到换能器(部分原因是我仍然觉得这个主题令人困惑)。double和之间是什么关系transduce
火星

21

假设您要使用一系列功能来转换数据流。Unix shell使您可以使用管道运算符执行此类操作,例如

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

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

Clojure也是如此。有多种方法可以表达转换的流程,但是根据您的操作方式,最终可以得到从一个函数传递到下一个函数的中间流。如果您有大量数据,将这些功能组合成一个功能会更快。换能器使其易于实现。Clojure较早的创新,减速器也可以使您做到这一点,但是有一些限制。传感器消除了一些限制。

因此,要回答您的问题,换能器不一定会使您的代码更短或更易理解,但是您的代码也不一定会变得更长或更不易理解,并且如果您要处理大量数据,换能器可以使您的代码快点。

是换能器的一个很好的概述。


1
嗯,换能器主要是性能优化,这是您在说的吗?
appshare.co 2014年

@Zubair是的,是的。注意,优化不仅仅是消除中间流;您也许还可以并行执行操作。
user100464 2014年

2
值得一提的是pmap,它似乎没有引起足够的重视。如果要map通过序列ping昂贵的函数,则使操作并行与添加“ p”一样容易。无需更改代码中的其他任何内容,现在就可以使用它-不提供alpha版本,不提供beta版本。(我猜想,如果函数创建中间序列,那么换能器可能会更快。)
2014年


8

我发现从converters-js中阅读示例可以帮助我更具体地理解它们,例如如何在日常代码中使用它们。

例如,考虑以下示例(取自上面链接的自述文件):

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);

换能器示例为何如此冗长。下划线版本看起来更加简洁
appshare.co 2014年

1
@Zubair不是真的t.into([], t.comp(t.map(inc), t.filter(isEven)), [0,1,2,3,4])
胡安·卡斯塔涅达

7

换能器是(据我所知!)函数,它们具有一个归约函数并返回另一个归约函数。归约函数是

例如:

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,那么该值是否为偶数?在第一种情况下,过滤器将该值传递到计数器,然后过滤下一个值。而不是先过滤然后将所有这些值传递给计数。

在第二个示例中是同一件事,它一次检查一个值,如果该值小于3,则让count加1。


我喜欢这个简单的解释
Ignacio

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

此外,在考虑子组时,它确实非常强大。例如,如果我们想知道布朗家族中有多少个孩子,我们可以执行:

;; 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

希望这些示例对您有所帮助。你可以在这里找到更多

希望能帮助到你。

克莱门西奥·莫拉莱斯·卢卡斯。


3
“换能器是一种构建算法转换的功能强大且可组合的方式,您可以在许多上下文中重复使用这些转换,并且它们已经应用于Clojure core和core.async。” 定义几乎可以适用于任何东西?
appshare.co 2014年

1
对于几乎所有的Clojure换能器,我都会说。
Clemencio Morales Lucas 2014年

6
与其说是定义,不如说是任务说明。
火星

4

我通过clojurescript 示例在博客上对此进行了博客介绍,该示例说明了序列函数现在如何能够通过替换归约函数进行扩展。

这就是我读到的换能器的要点。如果你想想cons或者conj是像操作硬编码的操作mapfilter等等,还原作用是不可到达的。

使用换能器,还原功能已解耦,由于换能器,我可以像使用本地javascript数组一样替换它push

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

filter 和朋友有一个新的1 arity操作,它将返回一个转导函数,您可以使用该函数提供自己的归约函数。


4

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

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

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

映射函数只是有时被称为“归约函数”的一类函数。另一个常见的归约函数是过滤器,它删除与谓词匹配的值(例如,删除所有偶数的值)。

换能器使您可以“包装”一个或多个还原功能的序列,并生成可在流或结构上工作的“包装”(本身就是一个功能)。例如,您可以“打包”一系列归约函数(例如,过滤偶数,然后映射结果数字以将它们递增1),然后在值的流或值结构(或两者)上使用该换能器“打包” 。

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

因此,给您带来的好处是您可以利用对这些功能的了解,并将其应用于更多的用例。给您带来的代价是,您必须学习一些额外的机械(例如,换能器)才能为您提供额外的动力。


2

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

由于该操作的实现不在输入的代码中,并且输出未做任何事情,因此换能器具有极高的可重用性。他们让我想起了Akka Streams中Flow

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


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.