Перевірте, чи містить список певне значення у Clojure


163

Який найкращий спосіб перевірити, чи містить список задане значення в Clojure?

Зокрема, поведінка компанії contains?зараз мене бентежить:

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

Я, очевидно, міг написати просту функцію, щоб пройти список і перевірити рівність, але, безумовно, повинен бути стандартний спосіб це зробити?


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! Схоже, я буду писати власну функцію в цьому випадку ... це мене трохи дивує, що в основній мові вже немає жодної.
mikera

4
Як сказав Міхал - у ядрі вже є функція, яка виконує те, що ви хочете: деякі.
котарак

2
Зверху Міхал прокоментував, (some #{101} '(100 101 102))сказавши, що "більшість часу це працює". Хіба не чесно сказати, що це завжди працює? Я використовую Clojure 1.4, і документація використовує подібний приклад. Це працює для мого і має сенс. Чи є якийсь особливий випадок, коли він не працює?
Девід Дж.

7
@DavidJames: Не працює, якщо ви перевіряєте наявність falseабо nil- див. Наступний параграф. На окремій примітці, в Clojure 1.5-RC1 contains?викидається виняток, коли в якості аргументу дається некерована колекція. Я припускаю, що я відредагую цю відповідь, коли вийде остаточний реліз.
Michał Marczyk

1
Це дурне! Основна відмінність колекції - відношення до членства. Це повинно було бути найважливішою функцією колекцій. en.wikipedia.org/wiki/Set_(mathematics)#Membership
jgomo3

132

Ось мій стандартний утиліт для тієї ж мети:

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

36
Це найпростіше і найбезпечніше рішення, оскільки воно також обробляє помилкові значення типу nilі false. Тепер чому це не частина клоджуру / ядра?
Стіан Сойланд-Рейєс

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
ІМХО це найкраща відповідь. Занадто погана клоджура містить? так заплутано названо.
mikkom

1
Поважний майстер Qc Na гуляв зі своїм учнем Антоном. Коли Антон розповів йому про проблеми з початківцями contains?, Qc Na вдарив його Bô і сказав: "Дурний студент! Ти повинен усвідомити, що ложки немає. Це все просто 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)) 

7

Оскільки це варто, це моя проста реалізація функції містить список для списків:

(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)))
Рафі Паноян

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. Ярлик на встановлення функції не працюватиме, якщо ви шукаєте помилкове значення ( 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 версію "список-містить?". Тепер він бере будь-яку кількість аргументів.

(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в реальному істинному / хибному присудку, наприклад

(defn in? [coll x] (if (some #{x} coll) true false))

чому if trueі false? someвже повертає значення true-ish та false-ish.
підрозділ

що про (деякі # {nil} [nil])? Він поверне нуль, який буде перетворений на хибний.
Вей Ци

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, ви можете так само легко викликати функцію .indexOfJava. Ця функція повертає індекс будь-якого елемента в колекції, і якщо він не може знайти цей елемент, повертає -1.

Скориставшись цим, ми могли б просто сказати:

(not= (.indexOf [1 2 3 4] 3) -1)
=> true

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)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.