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

欢迎!请浏览关于页面了解更多关于如何使用此操作的信息。

+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)))))

结果是相同的。

然而,据我所知,你不能在 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"

2个主要更改
- 使用 (interpose ", ") 变换器来模仿 clojure.string/join 的分隔符行为。如果您将结果变换器与 into 一起使用,它将返回这个: ["1 2" ", " "3 4"]。现在只剩下一个任务需要高效地将它构建成一个字符串。
- 使用适当的字符串构建器减少函数,提供所有 3 个参数,因为它们都是必需的:0-参数用于创建字符串构建器,2-参数用于实际减少步骤,该步骤将以 "1 2"", ""3 4" 的顺序调用,并完成 1-参数将字符串构建器转换为一个字符串

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

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

重新阅读变换器的签名 - 真是令人惊奇啊!清晰明了!
+1
by
编辑 by

在第二个管道示例中,使用 `->>`, 你正在进行类似于 `into` 版本的操作。通过获取与 `:tripData` 的关联来解包行程,然后获取与 `:segments` 的关联。不同之处在于,在 `into` 版本中,你提供了一个类似外观的管道(原因在此未详细说明,由 `comp` 来描述),这个管道实际上是一个花哨的循环,该循环遍历你正在归约的元素(`:segments 向量),并在应用隐式归约函数(此处可能是 `conj` 或 `conj!`,因为 `into` 将使用临时的向量来更有效地构建)之前在循环内相应地转换元素。你可以将其视为在归约过程中,所有 `comp` 中的函数按元素应用(从某种意义上说)。将其与第二个示例进行比较

第二个示例是基于惰性序列的,你将 `:segments 向量传送到一个通过映射和连接应用 `:dataPoints 并构建惰性序列的 `map`。然后再次将其 `map`(也产生另一个惰性序列,依赖于遍历 `mapcat` 的输出),不断地进行。因此,实际上有三个相互依赖的惰性序列的堆栈。要遍历管道的输出,我必须遍历最后的惰性序列,然后这个惰性序列遍历其依赖的前序序列,然后那个前序序列遍历 `mapcat`。这有点像过去的消防演习(现在人们使用水桶传递把水从水源运到火灾中,你必须把水桶传递给排在最后的一个人,让他倒水到火上)。序列化版本有一些开销,因为每个序列都必须为访问元素时分配一些中间对象、延迟求值函数和强制评估。这不是问题,但是存在开销,且随着你堆叠更多的序列,这种开销会增加。

我们不会在 `into` 变体中出现这些开销,因为我们从不创建中间惰性序列。相反,我们只是应用了一组函数(而不是创建、强制和缓存多个依赖序列的元素)。消除这种开销可以产生显著节省。

注意:如果使用 `transduce`,你不需要构建向量(正如 `into` 所做的那样)。你还可以通过 `sequence` 和 `eduction` 来混合匹配序列和转换器。

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

转换器很棒,既因为效率很高,也因为它们很通用,并且很好地与序列和 core.async 通道配合。这里有很多实用功能。

然而,据我所知,你不能在 comp 中使用 map 关键字(例如,:tripData,:segments),因为关键字不是传感器。

你可以(如所演示)使用 `map` 关键字,就像

(map :blah)

这样可以产生一个转换器。

我不知道如何在效率和学会如何使用传感器之间取得平衡。

你可能看到,如果在 `transduce` 的某个版本中构建字符串(比如 `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` 内部使用字符串构建器所做的,而不创建任何中间集合(如向量)。

by
谢谢你,汤姆!这里有很多非常有用的信息!
+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
嗨!

非常感谢!非常有趣!
...