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

最后,使用clojure.string/join得出相同的结果。

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

我不知道如何使这种方法更有效/更好,同时学习如何使用传感器。

我将非常感谢任何帮助/指导/反馈!

谢谢。

3个答案

+2

关于汤姆回答的如何使用字符串构建器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参数用于创建字符串构建器,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向量化到map中,该:segments向量化基于映射和串联:dataPoints的应用到输入上创建一个懒惰序列。然后再次对: segments向量进行: map操作(也产生另一个懒惰序列,根据: mapcat的输出进行遍历),然后再一次。所以,实际上这里有一个包含3个相关懒惰序列的“堆栈”。为了遍历流水线的输出,我必须遍历最后一个懒惰序列,然后该序列遍历它依赖的前驱序列,直到mapcat。这有点像消防演习(在过去的那些年,当人们用桶链手动从水源运水灭火时,你需要在人们之间传递水桶,最后一个人需要将水倒在火上)。在基于序列的版本中,有一些开销,因为每个序列都必须为必要的中间对象、thunks和强制评估分配内存。这不是严重的问题,但是确实存在一些开销,而且这种开销随着堆栈中序列数量的增加而增加。

但是使用into版本时没有这种开销,因为我们永远不会创建中间的懒惰序列。相反,我们只是应用一些函数(而不是创建、强制和缓存多个依赖序列的元素)。消除这种开销可以获得显著的节省。

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

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

转换器很酷,因为它不仅效率高,而且相当通用,非常适合与序列和核心异步通道一起使用。那里有很多实用工具。

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

您可以(如所演示的)使用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
感谢汤姆,那里有很多非常有用的信息!
+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"
嗨!

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