【问题标题】:Return first item in a map/list/sequence that satisfies a predicate返回满足谓词的映射/列表/序列中的第一项
【发布时间】:2012-04-28 20:57:58
【问题描述】:

我正在寻找一个函数,它返回 fn 计算结果为 true 的序列中的第一个元素。例如:

(first-map (fn [x] (= x 1)) '(3 4 1))

上面的假函数应该返回 1(列表中的最后一个元素)。 Clojure 中有这样的东西吗?

【问题讨论】:

  • 为什么不只是(first (filter #(= % 1) '(3 4 1))?
  • @4e6 因为这会将函数应用于列表中的每个元素,这在大型列表中可能是不可取的。
  • 地图是懒惰的,所以我认为不会。不过你应该测试一下。

标签: clojure


【解决方案1】:
user=> (defn find-first
         [f coll]
         (first (filter f coll)))
#'user/find-first
user=> (find-first #(= % 1) [3 4 1])
1

编辑:并发。 :) 不。它不适用于整个列表f。由于filter 的懒惰,仅对第一个匹配的元素进行。

【讨论】:

  • 感谢@kotarak,我担心这会处理集合中的所有元素,这在大列表中是不可取的。也许我需要创建一个重复的函数,直到找到满足条件的元素。
  • @Matthew 模分块序列。根据块大小,f 可以应用于更多元素。
  • 使用filter 或some 会占用更多资源。或者,reduced 可以简化整个计算,同时占用更少的资源:@​​987654328@。
  • @xando 你用类似下面的东西来证明了吗?user=> (def input (range)) #'user/input user=> (time (some #{50000000} input)) "Elapsed time: 3909.789411 msecs" 50000000 user=> (time (reduce #(when (= %2 50000000) (reduced %2)) nil input)) "Elapsed time: 198616.007581 msecs" 50000000这些恰好是我得到的结果。
  • @xando 实际上,您一开始说some 使用更多资源,而reduced 可以在占用更少资源的同时缩短整个计算。这仅在使用纯range 或其他可以直接减少自身的序列/集合的例外情况下才是正确的。另外,所有替代方案都明显短路(range 是一个无限序列)。
【解决方案2】:

在你的情况下,成语是

(some #{1} [1 2 3 4])

它是如何工作的:#{1} 是一个集合字面量。一个集合也是一个函数,如果 arg 存在于集合中,则评估其 arg,否则评估为 nil。任何集合元素都是“真实”值(嗯,除了布尔值 false,但这是集合中的罕见)。 some 返回针对结果为真的第一个集合成员评估的谓词的返回值。

【讨论】:

  • 如果您可以使用整个元素进行搜索,这很有用,但如果它们被部分索引,some 将不会返回复合元素(例如哈希图)。
  • (->> [nil nil nil nil 42 nil nil] (some identity))42
【解决方案3】:

我尝试了这个线程中提到的几种方法(JDK 8 和 Clojure 1.7),并做了一些基准测试:

repl> (defn find-first
         [f coll]
         (first (filter f coll)))
#'cenx.parker.strategies.vzw.repl/find-first

repl> (time (find-first #(= % 50000000) (range)))
"Elapsed time: 5799.41122 msecs"
50000000

repl> (time (some #{50000000} (range)))
"Elapsed time: 4386.256124 msecs"
50000000

repl> (time (reduce #(when (= %2 50000000) (reduced %2)) nil (range)))
"Elapsed time: 993.267553 msecs"
50000000

结果表明reduce 方式可能是 clojure 1.7 中最有效的解决方案。

【讨论】:

  • 非常有趣。感谢您测试这些,xando。如果有来自Criterium 的数字会更好,但我的猜测是,通过搜索序列中到目前为止的项目,JVM 有机会优化代码。
【解决方案4】:

In 2016 there was a patch submitted to clojure core 为(first (filter pred coll)) 成语添加了一个有效的快捷方式,它被称为seek。

该实现避免了(first (filter)) 和(some #(when (pred))) 替代方案所带来的问题。也就是说,它可以有效地处理分块序列,并且可以很好地处理 nil? 和 false? 谓词。

补丁:

(defn seek
  "Returns first item from coll for which (pred item) returns true.
   Returns nil if no such item is present, or the not-found value if supplied."
  {:added  "1.9" ; note, this was never accepted into clojure core
   :static true}
  ([pred coll] (seek pred coll nil))
  ([pred coll not-found]
   (reduce (fn [_ x]
             (if (pred x)
               (reduced x)
               not-found))
           not-found coll)))

例子:

(seek odd? (range)) => 1
(seek pos? [-1 1]) => 1
(seek pos? [-1 -2] ::not-found) => ::not-found
(seek nil? [1 2 nil 3] ::not-found) => nil

最终补丁被拒绝:

经审查,我们决定不包含此内容。使用线性搜索(尤其是嵌套线性搜索)会导致性能下降 - 通常最好使用其他类型的数据结构,这就是为什么过去没有包含此功能的原因。 ~亚历克斯·米勒 17 年 5 月 12 日下午 3:34

【讨论】:

【解决方案5】:

我认为some 是完成这项工作的最佳工具:

(some #(if (= % 1) %) '(3 4 1))

【讨论】:

  • 这不会受到分块序列效应的影响。但在1 为nil 或false 的情况下不起作用。 YMMV。
  • 等等... 1 怎么可能等于 nil? :) 无论如何,Matthew 指定元素应该“评估为真”,所以我认为这种行为是理想的。
  • 将f 视为#(contains? #{0 "false" false} %)。他说“将 fn 评估为 true”。
【解决方案6】:

使用drop-while 而不是filter 应该解决f 对于分块序列的“过度应用”:

(defn find-first [f coll]
  (first (drop-while (complement f) coll)))
;;=> #'user/find-first

(find-first #(= % 1) [3 4 1])
;;=> 1

【讨论】:

  • 很酷的解决方案:)。我进行了测试以确保懒惰。 (find-first #(do (println %) (even? %)) [1 3 2 5 7 9]),返回 2,不打印 5、7 或 9。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-08-20
  • 2015-08-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多