请在Clojure 2024 状态调查中分享您的想法!

欢迎!请查看关于页面了解如何使用本站。

+3
转换器

嗨,

我在学习转换器的相关知识,但在实践中遇到了一些瓶颈。

我想了解的是,它们是否适用于我的使用场景(以及作为一个学习练习),以及我所做的是否正确/高效/也许有更好的方法 :-)

假设我的数据结构如下

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

可能有一些成百上千的数据点,每个数据点都有一个单独的位置。

我想有效地提取纬度和经度到一个单独的集合,然后将其转换为字符串。我提出了以下方案

 (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

关于汤姆关于使用字符串构建器和reduce函数表述更好的答案,以下是如何做得更简洁的方法

(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 ", ") transducer来模拟clojure.string/join的分隔符行为。如果你使用结果的transducer与into一起使用,它将返回这个:["1 2" ", " "3 4"]。现在的任务就是高效地从这个结果构建字符串。
- 使用正确的字符串构建器减少函数,提供所有三个arity,因为都需要:0-arity用于创建字符串构建器,2-arity用于实际的减少步骤,该步骤将接收"1 2"", ""3 4"按顺序调用,以及1-arity用于将字符串构建器转换为字符串。

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


编辑
谢谢!我喜欢在那里创建自己的transducer的能力和这样做的理由!非常感谢!:-)

重新阅读transducer的签名——非常令人印象深刻!很清晰!
+1

编辑

在第二个管道示例中,使用 ->> ,你执行了与 into 版本类似的操作。通过取得与 :tripData 的关联来解包旅程,然后获取与 :segments 相关联的映射。区别在于,在 into 版本中,你正在向一个实际上是一个复杂的循环(遍历你要减少的东西,即 :segments 向量)提供一个类似外观的管道(这里的 comp 是基于某些未详细说明的原因),并在循环内相应地变换元素,然后再应用隐式减少函数(这里可能是 conj 或真正的是 conj!,因为 into 会使用一个瞬态向量来更有效地构建)。你可以认为在减少过程中实际上每个函数都应用于每个元素。与第二个例子进行比较。

在第二个基于懒序列的例子中,你将 :segments 向量通过到一个 map 中,该 map 基于映射和连接 :dataPoints 应用于输入来构建懒序列。然后再次 map(也产生了另一个懒序列,依赖于 mapcat 的输出),如此重复。所以,实际上存在一个由3个依赖懒序列构成的“堆”。为了遍历管道的输出,我必须遍历最后的懒序列,然后它遍历其依赖的前驱,然后是 mapcat。这里有一種如廚房救火操練(在以前的時候,當他們通過手動將水從來源運送到火源地時,你將必須將桶在一系列人中翻倒,最後一位將其倒入火中)。基于序列的版本有一些开销,因为每个序列都需要为中间对象、thunks 以及访问元素时执行的强制评估分配一些空间。这并不是非常严重,但确实有开销,且随着堆叠的序列数量增加,这种开销也在增长。

使用 into 变体没有这种开销,因为我们从未创建中间的懒序列。相反,我们只是应用了一组函数(而不是创建、强制和缓存多个依赖序列的元素)。消除这种开销可以带来显著的节省。

注意:你可以使用 transduce 来消除需要构建向量(如 into 所做的)的需求。你还可以通过 sequenceeduction 混合使用序列和转置器。

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

转置器既因其效率,也因其比较通用并且很好地与序列和核心异步通道结合而酷。这里有很多实用的功能。

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

你(如演示所示)可以对关键字进行 map,就像这样

(map :blah)

它生成一个转置器。

我困惑如何在提高效率的同时学习如何使用转换器。

你可能看到使用类似于 clojure.string/jointransduce 版本构建字符串是否比创建一个向量并将其发送到 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 在内部使用字符串构建器所做的操作,而不会创建任何中间集合(如向量)。

感谢汤姆,那里有很多非常有用的信息!
+1
by

xforms (https://github.com/cgrand/xforms) 中,存在一个 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"
by
嗨!

非常感谢!非常有意思!
...