在Clojure中测试列表是否包含特定值

194

在Clojure中,测试一个列表是否包含给定值的最佳方法是什么?

特别是,contains?函数的行为目前让我感到困惑:

(contains? '(100 101 102) 101) => false

我显然可以编写一个简单的函数来遍历列表并测试相等性,但肯定有一种标准方法可以做到这一点吧?


7
确实有点奇怪,contains? 是 Clojure 中最具误导性的函数名称 :) 希望 Clojure 1.3 版本会将其重命名为 contains-key? 或类似名称。 - j-g-faustus
4
我认为这个问题已经被反复讨论了很多次。包含的内容不会改变。请看这里:http://groups.google.com/group/clojure/msg/f2585c149cd0465d 和 http://groups.google.com/group/clojure/msg/985478420223ecdf。 - kotarak
1
@kotarak 感谢提供链接!我实际上同意 Rich 在使用 contains? 名称方面的观点,尽管我认为当应用于列表或序列时应该更改以引发错误。 - mikera
19个回答

238

啊,contains?... Clojure中前五个最受欢迎的FAQ之一。

contains?并不检查集合是否包含某个值;它检查是否可以使用get检索到一个项,换句话说,它检查集合是否包含一个键。这对于集合(可认为在键和值之间没有区别),映射(因此(contains? {:foo 1} :foo)true)和向量是有意义的(但请注意,(contains? [:foo :bar] 0)true,因为这里的键是索引,而涉及的向量确实“包含”索引0!)。

增加混淆的是,在不应调用contains?的情况下,它只会简单地返回false;这就是在(contains? :foo 1)(contains? '(100 101 102) 101)中发生的情况。更新:在Clojure ≥ 1.5中,contains?当操作一个不支持预期“键成员”测试的类型的对象时将抛出异常。

要执行您正在尝试的操作的正确方法如下:

; most of the time this works
(some #{101} '(100 101 102))

当搜索一组项目中的其中一个时,可以使用更大的集合;当搜索 false / nil 时,可以使用 false? / nil? — 因为 (#{x} x) 返回 x ,因此 (#{nil} nil)nil;当搜索多个项目中的某些项目可能是 falsenil 时,可以使用

(some (zipmap [...the items...] (repeat true)) the-collection)

(请注意,任何类型的集合都可以将项目传递给 zipmap 。)


4
如Michal所说 - 核心中已经有一个函数可以实现您想要的功能:some。 - kotarak
2
以上,Michal在评论(some #{101} '(100 101 102))时说“大多数情况下这是有效的”。难道不能说它总是有效的吗?我正在使用Clojure 1.4,文档中也使用了这种示例。它对我来说很有效并且很有意义。是否存在某些特殊情况它无法工作? - David J.
8
@DavidJames:如果您正在检查falsenil的存在,则不起作用--请参见以下段落。另外,对于Clojure 1.5-RC1,当给定非键集合作为参数时,contains?会抛出异常。我想等正式版发布后再编辑这个答案。 - Michał Marczyk
1
这太蠢了!集合的主要区别在于成员关系。这应该是集合中最重要的功能。https://en.wikipedia.org/wiki/Set_(mathematics)#Membership - jgomo3
2
@jgomo3,你可以使用contains?在集合上测试成员资格。但是你不能在映射或列表上使用它,因为这样做不再是O(1),而变成了O(n)。contains?应该是O(1),这就是为什么在这些情况下需要进行自己的线性搜索。 - Didier A.
显示剩余3条评论

159

这是我用于同一目的的标准工具:

(defn in? 
  "true if coll contains elm"
  [coll elm]  
  (some #(= elm %) coll))

42
这是最简单、最安全的解决方案,因为它还处理像 nilfalse 这样的假值。那么为什么这不是 clojure/core 的一部分呢? - Stian Soiland-Reyes
2
“seq” 可以考虑改名为 “coll”,以避免与函数“seq” 混淆。 - nha
3
你可以这样做,是的。在这里没有关系:因为我们没有在函数体内使用名为“seq”的函数,所以与同名参数没有冲突。但如果你认为重命名会使它更易于理解,随意编辑答案。 - j-g-faustus
2
值得注意的是,如果您不必担心nilfalse,那么这可能比(boolean (some #{elm} coll))慢3-4倍。 - neverfox
显示剩余2条评论

32

您可以始终使用 .methodName 语法调用 Java 方法。

(.contains [100 101 102] 101) => true

5
在我看来,这是最好的答案。可惜Clojure中包含的某些部分命名非常令人困惑。 - mikkom
5
尊敬的大师 Qc Na 正在和他的学生 Anton 散步。当 Anton 向他提起使用 contains? 函数时遇到了一些初学者问题时,Qc Na 用 Bô 打了他一下,并说:“愚蠢的学生!你必须意识到没有勺子。它底层都是 Java! 使用点符号。”就在那一刻,Anton 获得了启迪。 - David Tonhofer

18

我知道我来晚了一点,不过怎么样:

(contains? (set '(101 102 103)) 102)

在Clojure 1.4 中最终输出 true :)


3
(set '(101 102 103)) 相当于 %{101 102 103}。因此,你可以将答案写成 (contains? #{101 102 103} 102) - David J.
6
这种方法的缺点是需要将原始列表 (101 102 103) 转换为一个集合。 - David J.

16
(not= -1 (.indexOf '(101 102 103) 102))

这个方案可行,但下面的方法更好:

(some #(= 102 %) '(101 102 103)) 

有些情况下,如果没有匹配项,则返回nil而不是false。 - Darshan Chaudhary

7

这是我常用的标准工具之一,用于快速处理此类问题:

(defn seq-contains?
  "Determine whether a sequence contains a given item"
  [sequence item]
  (if (empty? sequence)
    false
    (reduce #(or %1 %2) (map #(= %1 item) sequence))))

是的,你的代码有优势,它会在找到匹配项后立即停止,而不是继续映射整个序列。 - G__

7

如果有用的话,这是我对列表实现contains函数的简单方法:

(defn list-contains? [coll value]
  (let [s (seq coll)]
    (if s
      (if (= (first s) value) true (recur (rest s) value))
      false)))

我们可以将谓词部分作为参数来请求吗?以获得像下面这样的东西: (defn list-contains? [pred coll value] (let [s (seq coll)] (if s (if (pred (first s) value) true (recur (rest s) value)) false))) - Rafi Panoyan

7

如果您有一个向量或列表,并想检查其中是否包含某个,您会发现contains?无法使用。

Michał已经解释了原因

; does not work as you might expect
(contains? [:a :b :c] :b) ; = false

在这种情况下,您可以尝试以下四件事:

  1. Consider whether you really need a vector or list. If you use a set instead, contains? will work.

    (contains? #{:a :b :c} :b) ; = true
    
  2. Use some, wrapping the target in a set, as follows:

    (some #{:b} [:a :b :c]) ; = :b, which is truthy
    
  3. The set-as-function shortcut will not work if you are searching for a falsy value (false or nil).

    ; will not work
    (some #{false} [true false true]) ; = nil
    

    In these cases, you should use the built-in predicate function for that value, false? or nil?:

    (some false? [true false true]) ; = true
    
  4. If you will need to do this kind of search a lot, write a function for it:

    (defn seq-contains? [coll target] (some #(= target %) coll))
    (seq-contains? [true false true] false) ; = true
    
此外,参见Michał的答案,了解检查序列中是否包含多个目标的方法。

5
以下是一种经典的Lisp解决方案:
(defn member? [list elt]
    "True if list contains at least one instance of elt"
    (cond 
        (empty? list) false
        (= (first list) elt) true
        true (recur (rest list) elt)))

4
在Clojure中,该方案不佳的原因是它在一个处理器上向上递归堆栈。更好的Clojure解决方案是<pre> (defn member? [elt col] (some #(= elt %) col)) </pre> 这是因为 some 可以在可用的核心上并行执行。 - Simon Brooke

4

我在j-g-faustus的版本的"list-contains?"的基础上进行了改进。现在它可以接受任意数量的参数。

(defn list-contains?
([collection value]
    (let [sequence (seq collection)]
        (if sequence (some #(= value %) sequence))))
([collection value & next]
    (if (list-contains? collection value) (apply list-contains? collection next))))

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接