2024年Clojure状态调查!中分享您的想法。

欢迎!请参阅关于页面以了解更多关于这个平台的信息。

+3
变压器

你好,

我在学习变压器,但遇到了一些障碍。

我想了解的是,它们是否适合使用(在我的用例中,但也是一个学习练习),我做的是否正确/高效/也许有更好的方法 :-)

比如说,我有一个这样子的数据结构

 (def trip {:tripData 
            {:segments [{:dataPoints 
                         [{:location {:lat 1 :lng 2}} 
                          {:location {:lat 3 :lng 4}}]}]}})

可能会有数百/数千个数据点,每个数据点都有一个单独的位置。

我想有效地提取出lat和lng到一个单独的集合中,并将其转换为字符串。我提出了以下方法

 (def xf
   (comp
    (mapcat :dataPoints)
    (map :location)
    (map (fn [{lat :lat lng :lng}] (str lat " " lng)))))

然后通过

(def lat-lng (into [] xf (->> trip :tripData :segments)))

我得到了类似这样的东西

["1 2" "3 4"]

然后(为了进行我的练习),进行以下操作

 (clojure.string/join ", " lat-lng)

最终获得这个

"1 2, 3 4"

这都没问题 :-)

然而,鉴于我对变压器的理解不深,我开始想是否有不同的/更好的方法。例如,在某些情况下,在comp xf中,将数据转换为字符串并在最后进行连接,而不是使用clojure.string/join。

我还发现,即使不使用变压器,也可以这样做

 (def lat-lng-2 (->> trip
                     :tripData
                     :segments
                     (mapcat :dataPoints)
                     (map :location)
                     (map (fn [{lat :lat lng :lng}] (str lat " " lng)))))

通过使用clojure.string/join,最终得到了相同的结果。

然而,我的理解是,你不能将一个map关键字(例如,:tripData, :segments)作为comp的一部分使用,因为关键字不是变压器。

我在如何提高效率和改进这一过程以及如何使用变压器方面一筹莫展。

我将非常感激一些帮助/指导/反馈!

谢谢。

3个答案

+2

在Tom对使用字符串构建器reducer进行改进的答案上进一步扩展,这里是更简洁的实现方式

(def xf
  (comp
   (mapcat :dataPoints)
   (map :location)
   (map (fn [{:keys [lat lng]}] (str lat " " lng)))
   (interpose ", ")))

(defn string-builder-rf
  ([] (StringBuilder.))
  ([^StringBuilder ret] (.toString ret))
  ([^StringBuilder acc in]
   (.append acc in)))

(transduce xf string-builder-rf (-> trip :tripData :segments))
; => "1 2, 3 4"

2个主要改动
- 使用 (interpose ", ") 传输器来模仿 clojure.string/join 的分隔符行为。如果你将结果传输器与 into 结合使用,它将返回以下内容:["1 2" ", " "3 4"]。现在,剩下的任务就是高效地将字符串从它构建出来。
- 使用合适的字符串构建器减少函数,提供所有三个版本,因为都需要它们:0-ary 用于创建字符串构建器,2-ary 用于实际减少步骤,该步骤将按顺序使用 "1 2"", ""3 4" 调用,以及1-ary 完成转换为字符串。

这将更高效,因为你避免了创建一个中间向量,该向量传递给 clojure.string/join

by
编辑 by
谢谢!我喜欢在那里创建自己的传输器和这么做的理由!非常感谢!:-)

重新阅读传输器的签名 - 真是令人惊叹的内容!清楚!
+1
by
编辑 by

在第二个管道示例中,使用 ->>,你正在进行类似于 into 版本的操作。通过检索与 :tripData 的关联来解开三元组,然后是该映射与 :segments 的关联。不同之处在于在 into 版本中,你提供了一个类似的外观管道(通过 comp 描述,原因在此没有详细介绍),作为一个实际的循环,该循环遍历被减少的元素(:segments 向量),并在循环内相应地转换元素,然后再应用隐式减少函数(这里可能是 conj,或者是真正的 conj!,因为 into 会使用一个短暂向量来构建更高效)。你可以将其视为在减少过程中(在某种意义上)将 comp 中的所有功能应用于每个元素。将其与第二个示例进行比较

在基于惰性序列的第二个例子中,您将:segments 向量输入到 map 中,形成一个基于映射和合并输入:dataPoints 应用结果的惰性序列。然后再次执行 map 操作(这会产生另一个基于 mapcat 输出的惰性序列,以及其他的)。事实上,这里实际上有一堆 3 个相互依赖的惰性序列。为了遍历管道的输出,我必须遍历最后一个惰性序列,然后它遍历其先前的依赖序列,依次遍历 mapcat。这有点像消防演习(在旧时代,当人们通过制作水桶接力手工从水源运水到火场时,你必须把水桶传递给一排人,最后一个人将水倒到火中)。在基于序列的版本中,由于每个序列都必须在访问元素时分配一些中间对象、thunks 并强制求值,所以存在一些开销。这并不严重,但确实存在开销,而随着你堆叠的序列越多,这种开销就越大。

由于我们毫不犹豫地创建这些中间惰性序列,所以在 into 变体中没有这种开销。相反,我们只是应用了一大批函数(而不是创建、强制和缓存多个依赖序列的元素)。消除这种开销可以带来显著的节省。

注意:如果你使用 transduce,就可以删除构建向量(如 into 所做的)的需求。你还可以通过 sequenceeduction 混合和匹配序列和转换器。

(sequence (eduction (map inc) (range 10)))

转换器(transducers)在效率方面很有用,因为它们相当通用,并且很好地与序列和 core.async 通道搭配使用。这里有很多效用。

然而,我的理解是,你不能将一个map关键字(例如,:tripData, :segments)作为comp的一部分使用,因为关键字不是变压器。

你可以(如演示所示)对关键字 map,例如

(map :blah)

这样就得到一个转换器。

我在如何提高效率和改进这一过程以及如何使用变压器方面一筹莫展。

你可以尝试使用类似 clojure.string/join 的方式构建字符串的版本,看看是否比创建向量和发送到 clojure.string/join 更快。
理想情况下,你会定义一个累加函数...

(let [res (->> trip
               :tripData
               :segments
               (transduce xf (completing (fn [^java.lang.StringBuilder acc x]
                                           (doto acc (.append sep) (.append (str x)))))
                          (java.lang.StringBuilder.)))]
  ;;get rid of the first comma
  (doto res (.deleteCharAt 0) str))

这应该能够复制 clojure.string/join 内部字符串构建器所做的工作,而不创建任何中间集合(例如向量)。

David Harrigan
感谢 Tom,提供了很多非常有用的信息!
+1
Christophe Grand

xformshttps://github.com/cgrand/xforms)中,有一个Directly from transduction构建字符串的 x/str 转换器上下文。

user=> (x/str 
         (comp
           (x/for [{:keys [dataPoints]} %
                   {{:keys [lat lng]} :location} dataPoints]
             (str lat " " lng))
           (interpose ", "))
         (-> trip :tripData :segments))
"1 2, 3 4"
嗨!

非常感谢!很有意义!
...