【问题标题】:Can someone explain Clojure Transducers to me in Simple terms?有人可以简单地向我解释 Clojure 转换器吗?
【发布时间】:2026-01-10 00:15:01
【问题描述】:

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

更新

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

【问题讨论】:

    标签: clojure transducer


    【解决方案1】:

    转换器是在不知道底层序列是什么(如何做)的情况下如何处理数据序列的方法。它可以是任何 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)
    

    【讨论】:

    • 我更多的是寻找一个带有示例的答案,该示例向我展示了传感器如何节省我的时间。
    • 这不是技术决定。我们只使用基于业务价值的决策。 “就用它们”会让我被解雇
    • 如果您延迟尝试使用传感器,直到 Clojure 1.7 发布,您可能更容易保住工作。
    • 转换器似乎是抽象各种形式的可迭代对象的有用方法。这些可以是非消耗性的,例如 Clojure seqs,也可以是消耗性的(例如异步通道)。在这方面,在我看来,如果您从基于 seq 的实现切换到使用通道的 core.async 实现,您将从使用转换器中受益匪浅。转换器应该允许您保持逻辑的核心不变。使用传统的基于序列的处理,您必须将其转换为使用传感器或一些核心异步模拟。这就是商业案例。
    • 我想知道是否存在基于“Transducer”的理论。它是函数式语言中的通用定义还是由 clojure 的作者发明的?它似乎将问题视为“chan”,“data flow”等
    【解决方案2】:

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

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

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

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

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

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

    【讨论】:

    • 啊,所以传感器主要是性能优化,是你说的吗?
    • @Zubair 是的,没错。请注意,优化不仅仅是消除中间流;您还可以并行执行操作。
    • 值得一提的是pmap,似乎没有得到足够的关注。如果您mapping 一个序列上的昂贵函数,那么使操作并行化就像添加“p”一样简单。无需更改代码中的任何其他内容,它现在可用——不是 alpha,不是 beta。 (如果函数创建中间序列,那么传感器可能会更快,我猜。)
    • 并行有利于最大限度地利用核心,但如果您正在处理大/无限数据,那么您将耗尽内存,因此延迟/流式传输将有所帮助
    【解决方案3】:

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

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

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

    【讨论】:

      【解决方案4】:

      我发现这篇文章可以让您更全面地了解换能器。

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

      【讨论】:

      【解决方案5】:

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

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

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

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

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

      那么这有什么特别之处呢?通常,减少函数无法有效组合以同时处理流和结构。

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

      【讨论】:

        【解决方案6】:

        转换器提高效率,让您以更模块化的方式编写高效代码。

        This is a decent run through.

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

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

        【讨论】:

        • 只是好奇,您在上面说过:“在每个步骤之间构建中间集合”。但是“中间集合”听起来不是一种反模式吗? .NET 提供惰性枚举,Java 提供惰性流或 Guava 驱动的迭代,惰性 Haskell 也必须具有惰性。这些都不需要map/reduce 来使用中间集合,因为它们都构建了一个迭代器链。我哪里错了?
        • Clojure mapfilter 在嵌套时创建中间集合。
        • 至少关于 Clojure 版本的惰性,这里的惰性问题是正交的。是的,map 和 filter 是惰性的,当你链接它们时也会为惰性值生成容器。如果你不抓住头,你就不会构建不需要的大型惰性序列,但你仍然会为每个惰性元素构建那些中间抽象。
        • 举个例子就好了。
        • @LyubomyrShaydariv 通过“中间集合”,noisesmith 并不是指“迭代/具体化整个集合,然后迭代/具体化另一个整个集合”。他或她的意思是,当您嵌套返回顺序的函数调用时,每个函数调用都会导致创建一个新的顺序。实际的迭代仍然只发生一次,但由于嵌套的顺序,存在额外的内存消耗和对象分配。
        【解决方案7】:

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

        示例: 归约函数是接受两个参数的函数:到目前为止的结果和输入。他们返回一个新的结果(到目前为止)。例如+:有两个参数,你可以认为第一个是目前的结果,第二个是输入。

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

        (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

        【讨论】:

        • 很好的解释,但对我来说很快就进入了太多的行话,“换能器生成的归约函数与结果的累积方式无关”。
        • 你说得对,这里生成的词不合适。
        • 它,好的。无论如何,我知道变形金刚现在只是一种优化,所以可能无论如何都不应该使用
        • 它们是减少函数的组合方式。你在哪里还有那个?这不仅仅是一种优化。
        • 我觉得这个答案很有趣,但我不清楚它是如何连接到传感器的(部分原因是我仍然觉得这个主题令人困惑)。 doubletransduce是什么关系?
        【解决方案8】:

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

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

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

        【讨论】:

          【解决方案9】:

          我使用 clojurescript example 对此进行了博文,其中解释了序列函数现在如何通过替换归约函数来扩展。

          这是我读到的传感器的重点。如果您考虑在 mapfilter 等操作中硬编码的 consconj 操作,那么归约函数是无法实现的。

          使用传感器,减少函数被解耦,我可以像使用原生 javascript 数组 push 一样替换它,这要感谢传感器。

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

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

          【讨论】:

            【解决方案10】:

            换能器的明确定义在这里:

            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
            

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

            希望对你有帮助。

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

            【讨论】:

            • “Transducer 是一种强大且可组合的方式来构建算法转换,您可以在许多情况下重用它,它们将出现在 Clojure 核心和 core.async 中。”定义几乎可以适用于任何事物?
            • 我会说几乎所有 Clojure 传感器。
            • 与其说是定义,不如说是使命宣言。
            【解决方案11】:

            Transducers 是(据我理解!)采用一个 reducing 函数并返回另一个函数的函数。 归约函数是

            例如:

            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。

            【讨论】:

            • 我喜欢这个简单的解释
            【解决方案12】:

            我发现阅读来自 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);
            

            【讨论】:

            • 传感器的例子怎么这么长。下划线版本看起来简洁很多
            • @Zubair 不是t.into([], t.comp(t.map(inc), t.filter(isEven)), [0,1,2,3,4])
            最近更新 更多