109

我已尝试阅读此内容,但我仍然不了解它们的价值或它们所取代的东西。他们是否使我的代码更短,更易于理解或什么?

更新

很多人都发布了答案,但是很高兴看到有和没有传感器的例子非常简单,即使是像我这样的白痴也能理解。除非当然传感器需要一定程度的理解,在这种情况下我永远不会理解他们:(

4

12 回答 12

79

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

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

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

在 Clojure 1.7 版之前,您可以通过三种方式编写数据流查询:

  1. 嵌套调用

    (reduce + (filter odd? (map #(+ 2 %) (range 0 10))))
    
  2. 功能组成

    (def xform
      (comp
        (partial filter odd?)
        (partial map #(+ 2 %))))
    (reduce + (xform (range 0 10)))
    
  3. 线程宏

    (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)
于 2014-10-12T07:33:04.917 回答
51

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

这是一个体面的贯穿

与编写对 old map、等的调用相比filterreduce您可以获得更好的性能,因为您不需要在每个步骤之间构建中间集合,并重复遍历这些集合。

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

于 2014-10-11T18:41:16.447 回答
28

假设您想使用一系列函数来转换数据流。Unix shell 允许你用管道操作符做这种事情,例如

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

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

Clojure 也是如此。有多种方法可以表示转换管道,但根据您的操作方式,您最终可能会得到从一个函数传递到下一个函数的中间流。如果您有大量数据,则将这些函数组合成一个函数会更快。传感器可以很容易地做到这一点。较早的 Clojure 创新,reducers,也可以让您这样做,但有一些限制。传感器消除了其中一些限制。

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

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

于 2014-10-11T22:18:49.917 回答
24

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

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

转换器现在可以采用 + 函数并使其成为两倍加函数(在添加之前将每个输入加倍)。这就是传感器的样子(用最基本的术语来说):

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

为了说明替换rfnwith+看看如何+转换为两倍加:

(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

于 2014-10-12T09:50:07.973 回答
11

Rich Hickey 在 Strange Loop 2014 会议上发表了“传感器”演讲(45 分钟)。

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

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

于 2014-10-16T10:52:05.677 回答
8

我发现阅读来自transducers-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看起来比使用下划线的通常替代方法要干净得多。

_.filter(_.map([0, 1, 2, 3, 4], inc), isEven);
于 2014-10-11T18:48:26.210 回答
8

换能器是(据我了解!)函数,它采用一个归约函数并返回另一个函数。约简函数是

例如:

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。

于 2014-10-14T17:18:59.053 回答
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}])

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

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

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

希望能帮助到你。

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

于 2014-10-16T11:01:36.127 回答
4

我在博客上写了一个 clojurescript示例,该示例解释了序列函数现在如何通过能够替换归约函数来扩展。

这是我读到的传感器的重点。如果您考虑在诸如等操作中硬编码的orcons操作conj,则归约函数是无法访问的。mapfilter

使用传感器,减少功能是解耦的,我可以像使用本机 javascript 数组一样替换它,push这要归功于传感器。

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

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

于 2014-12-19T17:24:35.367 回答
4

这是我的(主要是)行话和代码免费答案。

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

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

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

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

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

因此,对您的好处是您可以利用您对这些功能的了解并将它们应用于更多用例。你的代价是你必须学习一些额外的机器(即换能器)来给你这个额外的力量。

于 2017-11-21T23:43:36.400 回答
3

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

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

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

于 2018-05-21T10:07:08.517 回答
1

我发现这篇文章为您提供了换能器的更多鸟瞰图。

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

于 2018-03-09T09:14:22.190 回答