Test whether a list contains a specific value in Clojure

后端 未结 18 2201
自闭症患者
自闭症患者 2020-11-30 17:17

What is the best way to test whether a list contains a given value in Clojure?

In particular, the behaviour of contains? is currently confusing me:

相关标签:
18条回答
  • 2020-11-30 18:09
    (defn in?
      [needle coll]
      (when (seq coll)
        (or (= needle (first coll))
            (recur needle (next coll)))))
    
    (defn first-index
      [needle coll]
      (loop [index 0
             needle needle
             coll coll]
        (when (seq coll)
          (if (= needle (first coll))
            index
            (recur (inc index) needle (next coll))))))
    
    0 讨论(0)
  • 2020-11-30 18:10

    Ah, contains?... supposedly one of the top five FAQs re: Clojure.

    It does not check whether a collection contains a value; it checks whether an item could be retrieved with get or, in other words, whether a collection contains a key. This makes sense for sets (which can be thought of as making no distinction between keys and values), maps (so (contains? {:foo 1} :foo) is true) and vectors (but note that (contains? [:foo :bar] 0) is true, because the keys here are indices and the vector in question does "contain" the index 0!).

    To add to the confusion, in cases where it doesn't make sense to call contains?, it simply return false; this is what happens in (contains? :foo 1) and also (contains? '(100 101 102) 101). Update: In Clojure ≥ 1.5 contains? throws when handed an object of a type that doesn't support the intended "key membership" test.

    The correct way to do what you're trying to do is as follows:

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

    When searching for one of a bunch of items, you can use a larger set; when searching for false / nil, you can use false? / nil? -- because (#{x} x) returns x, thus (#{nil} nil) is nil; when searching for one of multiple items some of which may be false or nil, you can use

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

    (Note that the items can be passed to zipmap in any type of collection.)

    0 讨论(0)
  • 2020-11-30 18:11

    Since Clojure is built on Java, you can just as easily call the .indexOf Java function. This function returns the index of any element in a collection, and if it can't find this element, returns -1.

    Making use of this we could simply say:

    (not= (.indexOf [1 2 3 4] 3) -1)
    => true
    
    0 讨论(0)
  • 2020-11-30 18:12

    Here's a quick function out of my standard utilities that I use for this purpose:

    (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))))
    
    0 讨论(0)
  • 2020-11-30 18:14

    I've built upon j-g-faustus version of "list-contains?". It now takes any number of arguments.

    (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))))
    
    0 讨论(0)
  • 2020-11-30 18:15

    There are convenient functions for this purpose in the Tupelo library. In particular, the functions contains-elem?, contains-key?, and contains-val? are very useful. Full documentation is present in the API docs.

    contains-elem? is the most generic and is intended for vectors or any other clojure seq:

      (testing "vecs"
        (let [coll (range 3)]
          (isnt (contains-elem? coll -1))
          (is   (contains-elem? coll  0))
          (is   (contains-elem? coll  1))
          (is   (contains-elem? coll  2))
          (isnt (contains-elem? coll  3))
          (isnt (contains-elem? coll  nil)))
    
        (let [coll [ 1 :two "three" \4]]
          (isnt (contains-elem? coll  :no-way))
          (isnt (contains-elem? coll  nil))
          (is   (contains-elem? coll  1))
          (is   (contains-elem? coll  :two))
          (is   (contains-elem? coll  "three"))
          (is   (contains-elem? coll  \4)))
    
        (let [coll [:yes nil 3]]
          (isnt (contains-elem? coll  :no-way))
          (is   (contains-elem? coll  :yes))
          (is   (contains-elem? coll  nil))))
    

    Here we see that for an integer range or a mixed vector, contains-elem? works as expected for both existing and non-existant elements in the collection. For maps, we can also search for any key-value pair (expressed as a len-2 vector):

     (testing "maps"
        (let [coll {1 :two "three" \4}]
          (isnt (contains-elem? coll nil ))
          (isnt (contains-elem? coll [1 :no-way] ))
          (is   (contains-elem? coll [1 :two]))
          (is   (contains-elem? coll ["three" \4])))
        (let [coll {1 nil "three" \4}]
          (isnt (contains-elem? coll [nil 1] ))
          (is   (contains-elem? coll [1 nil] )))
        (let [coll {nil 2 "three" \4}]
          (isnt (contains-elem? coll [1 nil] ))
          (is   (contains-elem? coll [nil 2] ))))
    

    It is also straightforward to search a set:

      (testing "sets"
        (let [coll #{1 :two "three" \4}]
          (isnt (contains-elem? coll  :no-way))
          (is   (contains-elem? coll  1))
          (is   (contains-elem? coll  :two))
          (is   (contains-elem? coll  "three"))
          (is   (contains-elem? coll  \4)))
    
        (let [coll #{:yes nil}]
          (isnt (contains-elem? coll  :no-way))
          (is   (contains-elem? coll  :yes))
          (is   (contains-elem? coll  nil)))))
    

    For maps & sets, it is simpler (& more efficient) to use contains-key? to find a map entry or a set element:

    (deftest t-contains-key?
      (is   (contains-key?  {:a 1 :b 2} :a))
      (is   (contains-key?  {:a 1 :b 2} :b))
      (isnt (contains-key?  {:a 1 :b 2} :x))
      (isnt (contains-key?  {:a 1 :b 2} :c))
      (isnt (contains-key?  {:a 1 :b 2}  1))
      (isnt (contains-key?  {:a 1 :b 2}  2))
    
      (is   (contains-key?  {:a 1 nil   2} nil))
      (isnt (contains-key?  {:a 1 :b  nil} nil))
      (isnt (contains-key?  {:a 1 :b    2} nil))
    
      (is   (contains-key? #{:a 1 :b 2} :a))
      (is   (contains-key? #{:a 1 :b 2} :b))
      (is   (contains-key? #{:a 1 :b 2}  1))
      (is   (contains-key? #{:a 1 :b 2}  2))
      (isnt (contains-key? #{:a 1 :b 2} :x))
      (isnt (contains-key? #{:a 1 :b 2} :c))
    
      (is   (contains-key? #{:a 5 nil   "hello"} nil))
      (isnt (contains-key? #{:a 5 :doh! "hello"} nil))
    
      (throws? (contains-key? [:a 1 :b 2] :a))
      (throws? (contains-key? [:a 1 :b 2]  1)))
    

    And, for maps, you can also search for values with contains-val?:

    (deftest t-contains-val?
      (is   (contains-val? {:a 1 :b 2} 1))
      (is   (contains-val? {:a 1 :b 2} 2))
      (isnt (contains-val? {:a 1 :b 2} 0))
      (isnt (contains-val? {:a 1 :b 2} 3))
      (isnt (contains-val? {:a 1 :b 2} :a))
      (isnt (contains-val? {:a 1 :b 2} :b))
    
      (is   (contains-val? {:a 1 :b nil} nil))
      (isnt (contains-val? {:a 1 nil  2} nil))
      (isnt (contains-val? {:a 1 :b   2} nil))
    
      (throws? (contains-val?  [:a 1 :b 2] 1))
      (throws? (contains-val? #{:a 1 :b 2} 1)))
    

    As seen in the test, each of these functions works correctly when for searching for nil values.

    0 讨论(0)
提交回复
热议问题