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"

这都还好 :-)

然而,鉴于我对传感器的经验不足,我开始怀疑是否有不同的/更好的方法。例如,年在 中以将数据转换为字符串和最终连接到一起,而不是使用 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),因为关键字不是传感器。

我在如何使这个更高效/更好的同时了解如何使用传感器方面感到困惑。

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

谢谢。

3 个答案

+2

在Tom关于使用字符串构造函数还原器做得更好的回答中,稍作展开,以下是更为简洁的实现方式。

(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"]。现在剩下的事情就是有效地从这个结果中构建字符串。
使用适当的字符串构建函数,提供所有三个参数性,因为它们都需要:0-arity用于创建字符串构建器,2-arity实际上是调用包含"1 2"", ""3 4"的还原步骤,1-arity用于将字符串构建器转换为字符串。

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

谢谢!我喜欢你那里创建自己的还原器,以及这么做的原因!非常感谢!:-)

再次阅读还原器的签名——非常惊人的东西!很清晰!

在第二个管道示例中,使用 ->>,你执行的是类似于 into 版本的类似操作。通过获取与 :tripData 的关联来解包行程,然后获取该映射与 :segments 的关联。区别在于,在 into 版本中,你提供了一个类似外观的管道(此处不详细说明使用 comp 的原因)到实际上是一个遍历你正在缩减的对象(即 :segments 向量)的流行循环,并在循环内相应地转换元素,然后在应用隐式缩减函数之前(这里可能是 conj,或者实际上是 conj!,因为 into 将使用暂时性向量来更高效地构建)。你可以将其视为在缩减过程中(在某种程度上)逐个元素应用 comp 中的所有函数。将其与第二个示例进行比较

在第二个基于惰性序列的例子中,你将 :segments 向量通过网络映射到一个构建基于映射和连接对 :dataPoints 应用进行的应用的惰性序列。然后再次进行 map(产生另一个惰性序列,这取决于 mapcat 的输出),再次。因此,实际上有个3个相互依赖的惰性序列的堆栈。为了遍历管道的输出,我必须遍历最后一个惰性序列,然后它遍历其依赖的前一个序列,然后遍历 mapcat。这个有点像消防演练(在旧时候,他们用来手动将水源搬运到火源,你需要将桶子沿着一堆人传递,最后一个人将其倒入火中)。基于序列版本中存在一些开销,因为每个序列都需要为一些中间对象、求值(thunks)和只需访问元素时的强制求值分配空间。这并不是大问题,但是有开销,而且这个开销随着堆叠的序列数量增加而增长。

我们不需要通过 into 变体产生的那个开销,因为我们从未创建过中间的惰性序列。相反,我们只是应用了大量的函数(而不是创建、强制、缓存多个依赖序列的元素)。消除这种开销可以产生显著节约。

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

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

转换器不仅因为效率高,还因为它们相当通用,并且很好地与序列和 core.async 信道集成。这里有很多实用工具。

然而,我的理解是,你不能使用一个 map 关键字(即,:tripData, :segments),因为关键字不是传感器。

你可以(如演示所示)对关键字进行 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 内部使用字符串构建器所做的工作,而不创建任何中间集合(例如向量)。

by
感谢Tom,有很多非常实用的信息!
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
嗨!

非常感谢您的内容!非常有意思!
...