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

欢迎!请查看关于页面以了解更多有关该功能的详细信息。

+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

关于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"]。现在只需有效地从其中构建字符串即可。
- 使用正确的字符串构建器减少函数,提供所有3种用法,因为它们都是必需的:0-arity用于创建字符串构建器,2-arity用于实际的减少步骤,该步骤将依次以"1 2"", ""3 4"调用,1-arity用于将字符串构建器转换为字符串

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

by
谢谢!我喜欢在那里创建自己的转换器及其理由!非常感谢!:-)

重新阅读转换器的签名——非常惊人的东西!明了!
+1投票< meta itemprop="upvoteCount" content="1">
by
编辑 by

在第二个管道示例中,使用->>,你做了与into版本类似的操作。通过获取与:tripData关联来解开三元组,然后是那个映射与:segments的关联。与into版本不同的是,在这个版本中,你正在将类似的长管道(通过comp描述,原因不详细说明)供入实际上是一个复杂的循环,遍历要减少的东西(:segments向量),并在循环内部相应地转换元素,然后在应用隐式减少函数之前(在这里可能是conj,实际上很可能是conj!,因为into将使用transient向量来更有效地构建)。您可以将所有在comp中的函数(在某种程度上)视为在减少过程中对每个元素应用。与第二个示例比较

在第二个基于惰性序列的例子中,你将:segments 向量管道输入到一个 map 中,它基于映射和连接应用:dataPoints 到输入来构造一个惰性序列。然后再次对这个 map 进行操作(也生成了另一个惰性序列,依赖于遍历 mapcat 的输出),反复如此。因此,实际上有3个相互依赖的惰性序列栈。为了遍历管道的输出,我必须遍历最后的惰性序列,它接着遍历其依赖的前置序列,后者遍历 mapcat。这像是一种消防训练(在年轻时,他们用木桶接力从水源往火灾中打水,你必须沿着一排人传递水桶,最后一个人将水倒在火灾上)。基于序列的版本有一些开销,因为每个序列都必须在访问元素时分配一些中间对象、thunks 和强制评估。这不是什么大问题,但确实有开销,而当堆叠的序列越多时,这种开销就越大。

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

注意:如果您使用 transduce,您可以消除建立向量(如 into 所做的那样)的需要。您还可以通过 sequenceeduction 混合和匹配序列和转换器。

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

转换器对于效率和通用性都相当不错,因为它们非常适合序列和 core.async 通道。这里有很多实用功能。

然而,我的理解是不能使用 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 内部的字符串构建器所做的工作,而不创建任何中间集合(如向量)。

感谢 Tom,提供了大量非常有用的信息!
+1投票< meta itemprop="upvoteCount" content="1">

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"
嗨!

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