Проверьте, содержит ли список конкретное значение в Clojure

163

Каков наилучший способ проверить, содержит ли список заданное значение в Clojure?

В частности, поведение contains?меня сейчас смущает:

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

Я мог бы, очевидно, написать простую функцию для обхода списка и проверки на равенство, но наверняка должен быть стандартный способ сделать это?

mikera
источник
7
Странно, действительно, содержит? должна быть самой обманчиво названной функцией в Clojure :) Надеемся, что Clojure 1.3 увидит, что она переименована в содержит ключ? или похожие.
jg-faustus
4
Я думаю, что сейчас об этом говорят несколько раз. содержит? не изменится. См. Здесь: groups.google.com/group/clojure/msg/f2585c149cd0465d и groups.google.com/group/clojure/msg/985478420223ecdf
котарак
1
@kotarak спасибо за ссылку! Я на самом деле согласен с Ричем здесь с точки зрения использования содержит? хотя я думаю, что имя должно быть изменено, чтобы
выдать

Ответы:

204

Ах, contains?якобы один из пяти часто задаваемых вопросов о: Clojure.

Он не проверяет, содержит ли коллекция значение; он проверяет, можно ли получить элемент с помощью 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; при поиске одного из нескольких элементов, некоторые из которых могут быть falseили nil, вы можете использовать

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

(Обратите внимание, что элементы могут быть переданы zipmapв любой тип коллекции.)

Михал Марчик
источник
Спасибо, Михал, - ты как всегда мудрец Clojure! Похоже, я собираюсь написать свою собственную функцию в этом случае ... меня немного удивляет, что ее нет в базовом языке.
Микера
4
Как сказал Михал, в ядре уже есть функция, которая делает то, что вы хотите: некоторые.
Котарак
2
Выше Михал прокомментировал (some #{101} '(100 101 102))высказывание, что «большую часть времени это работает». Разве не справедливо сказать, что это всегда работает? Я использую Clojure 1.4, и в документации используется такой пример. Это работает для меня и имеет смысл. Есть ли какой-то особый случай, когда он не работает?
Дэвид Дж.
7
@DavidJames: не работает, если вы проверяете наличие falseили nil- см. Следующий абзац. Отдельно отметим, что в Clojure 1.5-RC1 contains?выдает исключение, когда в качестве аргумента приводится неключевая коллекция. Полагаю, я отредактирую этот ответ, когда выйдет финальная версия.
Михал Марчик
1
Это глупо! Основным отличием коллекции является членство в отношениях. Это должно было быть самой важной функцией для коллекций. en.wikipedia.org/wiki/Set_(matmatics)#Membership
jgomo3
132

Вот мой стандартный утилит для той же цели:

(defn in? 
  "true if coll contains elm"
  [coll elm]  
  (some #(= elm %) coll))
JG-Фауст
источник
36
Это самое простое и безопасное решение, поскольку оно также обрабатывает ложные значения, такие как nilи false. Теперь, почему это не является частью clojure / core?
Стиан Соиланд-Рейес,
2
seqМожет быть, можно переименовать coll, чтобы избежать путаницы с функцией seq?
nha
3
@nha Вы можете сделать это, да. Здесь это не имеет значения: поскольку мы не используем функцию seqвнутри тела, нет конфликта с параметром с тем же именем. Но не стесняйтесь редактировать ответ, если вы думаете, что переименование облегчит его понимание.
jg-faustus
1
Стоит отметить, что это может в 3-4 раза медленнее, чем (boolean (some #{elm} coll))если вам не о чем беспокоиться nilили false.
neverfox
2
@AviFlax Я думал о clojure.org/guides/threading_macros , где говорится: «По соглашению, основные функции, которые работают с последовательностями, ожидают последовательность в качестве своего последнего аргумента. Соответственно, конвейеры содержат карту, фильтруют, удаляют, уменьшают в и т. Д. обычно называют макрос - >> Но я предполагаю, что соглашение больше касается функций, которые работают с последовательностями и возвращаемыми последовательностями.
Джон Уайзман
18

Вы всегда можете вызывать Java-методы с синтаксисом .methodName.

(.contains [100 101 102] 101) => true
Юрий Литвинов
источник
5
ИМХО это лучший ответ. Слишком плохой clojure содержит? так смутно назван.
Mikkom
1
Достопочтенный мастер Qc Na гулял со своим учеником Антоном. Когда Антон рассказал ему о проблеме с каким-то новичком contains?, Qc Na ударил его Бо и сказал: «Глупый ученик! Ты должен понять, что ложки нет. Это всего лишь Java внизу! Используйте точечную запись». В этот момент Антон стал просветленным.
Дэвид Тонхофер
17

Я знаю, что я немного опоздал, но как насчет:

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

Наконец-то в clojure 1.4 выводы верные :)

Джулиани Деон
источник
3
(set '(101 102 103))так же, как %{101 102 103}. Таким образом, ваш ответ может быть записан как (contains? #{101 102 103} 102).
Дэвид Дж.
4
Это имеет недостаток, заключающийся в необходимости преобразования исходного списка '(101 102 103)в набор.
Дэвид Дж.
12
(not= -1 (.indexOf '(101 102 103) 102))

Работает, но ниже лучше:

(some #(= 102 %) '(101 102 103)) 
jamesqiu
источник
7

Для чего это стоит, это моя простая реализация функции содержит списки:

(defn list-contains? [coll value]
  (let [s (seq coll)]
    (if s
      (if (= (first s) value) true (recur (rest s) value))
      false)))
mikera
источник
Можем ли мы попросить предикатную часть в качестве аргумента? Чтобы получить что-то вроде:(defn list-contains? [pred coll value] (let [s (seq coll)] (if s (if (pred (first s) value) true (recur (rest s) value)) false)))
Рафи Паноян
6

Если у вас есть вектор или список и вы хотите проверить, содержится ли в нем значение , вы обнаружите, что contains?оно не работает. Михал уже объяснил почему .

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

В этом случае вы можете попробовать четыре вещи:

  1. Подумайте, действительно ли вам нужен вектор или список. Если вы используете набор вместо , contains?будет работать.

    (contains? #{:a :b :c} :b) ; = true
  2. Используйтеsome , оборачивая цель в наборе, следующим образом:

    (some #{:b} [:a :b :c]) ; = :b, which is truthy
  3. Ярлык set-as-function не будет работать, если вы ищете ложное значение ( falseили nil).

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

    В этих случаях вам следует использовать встроенную функцию предиката для этого значения false?или nil?:

    (some false? [true false true]) ; = true
  4. Если вам нужно много искать, напишите для него функцию :

    (defn seq-contains? [coll target] (some #(= target %) coll))
    (seq-contains? [true false true] false) ; = true

Также см . Ответ Михала о том, как проверить, содержится ли какая-либо из нескольких целей в последовательности.

Рори О'Кейн
источник
5

Вот краткая функция из моих стандартных утилит, которые я использую для этой цели:

(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__
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он потенциально параллелен между доступными ядрами.
Саймон Брук
4

Я основывался на версии jg-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))))
Урс Рупке
источник
2

Это так же просто, как использовать набор - аналогично картам, вы можете просто поместить его в положение функции. Это оценивает к значению, если в наборе (который является правдивым) или nil(который является ложным):

(#{100 101 102} 101) ; 101
(#{100 101 102} 99) ; nil

Если вы проверяете вектор / список разумного размера, которого у вас не будет до времени выполнения, вы также можете использовать setфункцию:

; (def nums '(100 101 102))
((set nums) 101) ; 101
Брэд Кох
источник
1

Рекомендуемый способ - использовать someс набором - см. Документацию для clojure.core/some.

Вы можете использовать someреальный предикат true / false, например

(defn in? [coll x] (if (some #{x} coll) true false))
KingCode
источник
почему if trueи false? someуже возвращает значения true-ish и false-ish.
Subub
что насчет (немного # {ноль} [ноль])? Он вернет ноль, который будет преобразован в ложь.
Вэй Цю
1
(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))))))
Дэвид
источник
1
(defn which?
 "Checks if any of elements is included in coll and says which one
  was found as first. Coll can be map, list, vector and set"
 [ coll & rest ]
 (let [ncoll (if (map? coll) (keys coll) coll)]
    (reduce
     #(or %1  (first (filter (fn[a] (= a %2))
                           ncoll))) nil rest )))

пример использования (который? [1 2 3] 3) или (который? # {1 2 3} 4 5 3)

Майкл
источник
до сих пор нет функции языка ядра?
matanster
1

Поскольку Clojure построен на Java, вы также можете легко вызвать .indexOf функцию Java. Эта функция возвращает индекс любого элемента в коллекции, и если она не может найти этот элемент, возвращает -1.

Используя это, мы могли бы просто сказать:

(not= (.indexOf [1 2 3 4] 3) -1)
=> true
AStanton
источник
0

Проблема с «рекомендуемым» решением состоит в том, что оно разрывается, когда значение, которое вы ищете, - «ноль». Я предпочитаю это решение:

(defn member?
  "I'm still amazed that Clojure does not provide a simple member function.
   Returns true if `item` is a member of `series`, else nil."
  [item series]
  (and (some #(= item %) series) true))
Саймон Брук
источник
0

Для этого есть удобные функции в библиотеке Тупело . В частности, функция contains-elem?, contains-key?и contains-val?очень полезна. Полная документация представлена в документации по API .

contains-elem?является наиболее общим и предназначен для векторов или любой другой ситуации 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))))

Здесь мы видим, что для целочисленного диапазона или смешанного вектора, contains-elem?работает как ожидалось для существующих и несуществующих элементов в коллекции. Для карт мы также можем искать любую пару ключ-значение (выраженную в виде вектора len-2):

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

Также легко найти набор:

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

Для карт и наборов проще (и более эффективно) использовать contains-key?для поиска записи карты или элемента набора:

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

А для карт вы также можете искать значения с помощью 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)))

Как видно из теста, каждая из этих функций работает правильно при поиске nilзначений.

Алан Томпсон
источник
0

Другой вариант:

((set '(100 101 102)) 101)

Используйте java.util.Collection # Содержит ():

(.contains '(100 101 102) 101)
Alex
источник