2024 年 Clojure 调查问卷 中分享您的想法!

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

+3
Transducers

嗨,

我正在学习更多关于转换单元的知识,但遇到了一些 deadends。

我想了解的是,是否适合在我的使用案例中使用(包括作为学习练习),以及我所做的是否正确/高效/可能还有更好的方法 :-)

假设我有一个这样的数据结构

 (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,最终得到了相同的结果。

然而,我的理解是不能在 comp 使用 map 关键字(即,:tripData, :segments)作为部分,因为关键字不是转换单元。

我在如何使这更高效/更好以及如何使用转换单元方面陷入了困境。

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

谢谢。

3 个答案

+2

关于汤姆如何用字符串构建器减少器做得更好的回答做一些扩展,以下是如何更干净地完成它的方法

(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"

两个主要更改
- 使用(interpse ", ")转换器来模拟clojure.string/join分隔符行为。如果您使用结果转换器与into结合,它将返回:["1 2" ", " "3 4"]。现在只需高效地从这些中构建字符串。
- 使用适当的字符串构建函数Reducing,提供了所有3种形式,因为它们都是必需的:0参数用于创建字符串构建函数,2参数用于实际的减少步骤,该步骤将按照“1 2”,“”, ”和“3 4”的顺序调用,并完成1参数将字符串构建函数转换为字符串。

这将更有效率,因为您避免了传递给clojure.string/join的中间向量化


编辑
谢谢!我喜欢那里能够创建自己的转换器,以及这样做的原因!非常感谢!:-)

再次阅读转换器签名——真是太令人惊讶了!太明确了!
+1

编辑

在第二个管道示例中,使用->>,您正在执行与into版本类似的操作。通过获取与:tripData关联的内容撤包,然后与该图与:segments的关联。不同之处在于,在into版本中,您实际上提供了一种类似的管道(通过comp描述,但未详细说明原因)到本质上是一个循环,该循环遍历您正在减少的内容(:segments向量),并在循环内部相应地转换元素,然后在应用隐式减少函数之前执行(这里可能是conj,或者可以说是conj!,因为into将使用临时向量来构建更有效地)。您可以将这些函数在comp中的应用视为在每个元素上(在某种程度上)执行减少操作。将此与第二个示例进行比较

在第二个基于惰性序列的例子中,你将:segments向量通过管道输入到map中,该函数根据映射和连接对输入数据点应用操作创建了一个惰性序列。然后再次进行map操作(也产生了另一个惰性序列,这取决于如何遍历mapcat的输出),重复这个过程。因此,实际上有三个相互依赖的惰性序列堆叠在一起。为了遍历管道的输出,我必须遍历最后一个惰性序列,它然后遍历其依赖的前驱序列,后者再遍历mapcat。这就像是一种古老的消防演习(在老一辈消防员用桶队从水源提水灭火时,你需要在人们排成一列传递水桶,最后一个人将水倒到火上)。基于序列版本的这种方法中存在一些开销,因为每个序列都需要分配一些中间对象、闭包以及 accessed时的强制评价。这并不是一个严重的问题,但是确实存在开销,而且随着序列数量堆叠的增长,这种开销会变大。

而在into版本中,我们没有这种开销,因为我们从未创建中间的惰性序列。相反,我们只是应用了一系列函数(而不是创建、强制和缓存多个依赖序列的元素)。消除这种开销可以产生显著的效果。

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

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

转换器在效率上很受欢迎,而且因为它们相当通用,很好地与序列和core.async通道兼容,所以具有很多实用价值。

然而,我的理解是不能在 comp 使用 map 关键字(即,:tripData, :segments)作为部分,因为关键字不是转换单元。

你可以(如演示所示)用map关键字进行操作

(map :blah)

这将生成一个转换器。

我在如何使这更高效/更好以及如何使用转换单元方面陷入了困境。

你可以尝试使用类似于clojure.string/join的版本transduce来构建字符串,看看它是否比创建向量并传输到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的内部分段构造函数,而不创建任何中间集合(如向量)。

by
谢谢Tom,这里有很多非常有用的信息!
+1
by

xformshttps://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
嗨!

感谢您的评论!非常有趣!
...