Sprawdź, czy lista zawiera określoną wartość w Clojure

163

Jaki jest najlepszy sposób sprawdzenia, czy lista zawiera daną wartość w Clojure?

W szczególności contains?wprawia mnie w zakłopotanie zachowanie :

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

Mógłbym oczywiście napisać prostą funkcję, aby przejść przez listę i przetestować pod kątem równości, ale z pewnością musi istnieć standardowy sposób zrobienia tego?

mikera
źródło
7
Rzeczywiście dziwne, zawiera? musi być najbardziej myląco nazwaną funkcją w Clojure :) Masz nadzieję, że Clojure 1.3 zobaczy, że zostanie zmieniona na zawiera-klucz? lub podobne.
jg-faustus
4
Myślę, że mówi się o tym kilka razy na śmierć. zawiera? nie zmieni się. Zobacz tutaj: groups.google.com/group/clojure/msg/f2585c149cd0465d and groups.google.com/group/clojure/msg/985478420223ecdf
kotarak
1
@kotarak dzięki za link! Właściwie zgadzam się z Rich tutaj w zakresie korzystania z zawartości? nazwa chociaż myślę, że powinna zostać zmieniona, aby zgłosić błąd po zastosowaniu do listy lub sekwencji
mikera.

Odpowiedzi:

204

Ach, contains?podobno jedno z pięciu najczęściej zadawanych pytań dotyczących Clojure.

To ma nie sprawdzić, czy kolekcja zawiera wartość; sprawdza, czy element można pobrać za pomocą getlub, innymi słowy, czy kolekcja zawiera klucz. Ma to sens w przypadku zbiorów (które można uważać za nie rozróżniające kluczy i wartości), map (tak samo (contains? {:foo 1} :foo)jest true) i wektorów (ale zauważ, że (contains? [:foo :bar] 0)to jest true, ponieważ klucze są tutaj indeksami, a dany wektor „zawiera” indeks 0!).

Aby dodać do zamieszania, w przypadkach, gdy dzwonienie nie ma sensu contains?, po prostu wraca false; to się dzieje w (contains? :foo 1) i także (contains? '(100 101 102) 101) . Aktualizacja: W Clojure ≥ 1,5 contains?wyrzuca, gdy wręczany jest obiekt typu, który nie obsługuje zamierzonego testu „kluczowego członkostwa”.

Prawidłowy sposób na zrobienie tego, co próbujesz zrobić, jest następujący:

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

Szukając jednego z kilku przedmiotów, możesz użyć większego zestawu; szukając false/ nil, możesz użyć false?/ nil?- ponieważ (#{x} x)zwraca x, więc (#{nil} nil)jest nil; podczas wyszukiwania jednej z wielu pozycji, z których niektóre mogą być falselub nilktórych możesz użyć

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

(Zwróć uwagę, że elementy można przekazywać do zipmapdowolnego typu kolekcji).

Michał Marczyk
źródło
Dzięki Michale - jak zwykle jesteś czcionką mądrości Clojure! Wygląda na to, że w tym przypadku napiszę własną funkcję ... nieco zaskakuje mnie, że nie ma jej jeszcze w podstawowym języku.
mikera 14.07.10
4
Jak powiedział Michał - w rdzeniu jest już funkcja, która robi to, czego pragniesz: niektóre.
kotarak
2
Powyżej Michał skomentował (some #{101} '(100 101 102)), że powiedział, że „przez większość czasu to działa”. Czy nie można powiedzieć, że to zawsze działa? Używam Clojure 1.4 i dokumentacja używa tego rodzaju przykładu. Działa na mnie i ma sens. Czy jest jakiś szczególny przypadek, w którym to nie działa?
David J.,
7
@DavidJames: Nie działa, jeśli sprawdzasz obecność falselub nil- zobacz następny akapit. Odrębną uwagę, w Clojure 1.5-RC1 contains?zgłasza wyjątek, gdy jako argument podano kolekcję bez klucza. Przypuszczam, że poprawię tę odpowiedź, gdy pojawi się ostateczna wersja.
Michał Marczyk
1
To jest głupie! Głównym wyróżnikiem zbioru jest relacja członkostwa. Powinna to być najważniejsza funkcja dla kolekcji. en.wikipedia.org/wiki/Set_(mathematics)#Membership
jgomo3
132

Oto moje standardowe narzędzie do tego samego celu:

(defn in? 
  "true if coll contains elm"
  [coll elm]  
  (some #(= elm %) coll))
jg-faustus
źródło
36
Jest to najprostsze i najbezpieczniejsze rozwiązanie, ponieważ obsługuje również fałszywe wartości, takie jak nili false. Dlaczego to nie jest częścią clojure / core?
Stian Soiland-Reyes
2
seqmożna zmienić nazwę na coll, aby uniknąć pomyłki z funkcją seq?
nha
3
@nha Mógłbyś to zrobić, tak. Nie ma to znaczenia: ponieważ nie używamy funkcji seqwewnątrz ciała, nie ma konfliktu z parametrem o tej samej nazwie. Możesz jednak edytować odpowiedź, jeśli uważasz, że zmiana nazwy ułatwiłaby jej zrozumienie.
jg-faustus
1
Warto zauważyć, że może to być 3-4 razy wolniejsze niż w (boolean (some #{elm} coll))przypadku, gdy nie musisz się martwić nillub false.
neverfox
2
@AviFlax Myślałem o clojure.org/guides/threading_macros , gdzie jest napisane: „Zgodnie z konwencją, podstawowe funkcje działające na sekwencjach oczekują sekwencji jako ostatniego argumentu. W związku z tym potoki zawierające mapowanie, filtrowanie, usuwanie, redukowanie, do itp. zwykle wywołuje makro - >>. " Ale myślę, że konwencja dotyczy bardziej funkcji, które operują na sekwencjach i zwracają sekwencje.
John Wiseman
18

Zawsze możesz wywołać metody Java za pomocą składni .methodName.

(.contains [100 101 102] 101) => true
Jurij Litwinow
źródło
5
IMHO to najlepsza odpowiedź. Zbyt złe ubranie zawiera? jest tak myląco nazwany.
mikkom
1
Czcigodny mistrz Qc Na spacerował ze swoim uczniem Antonem. Kiedy Anton powiedział mu o problemie jakiegoś początkującego contains?, Qc Na uderzył go Bô i powiedział: „Głupi uczniu! Musisz zdać sobie sprawę, że nie ma łyżki. Pod spodem jest tylko Java! Użyj notacji kropkowej.”. W tym momencie Anton osiągnął oświecenie.
David Tonhofer,
17

Wiem, że jestem trochę spóźniony, ale co z:

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

W końcu w clojure 1.4 wyświetla prawdę :)

Giuliani Deon
źródło
3
(set '(101 102 103))jest taki sam jak %{101 102 103}. Twoja odpowiedź może być zapisana jako (contains? #{101 102 103} 102).
David J.,
4
Ma to tę wadę, że wymaga konwersji oryginalnej listy '(101 102 103)do zestawu.
David J.,
12
(not= -1 (.indexOf '(101 102 103) 102))

Działa, ale poniżej jest lepiej:

(some #(= 102 %) '(101 102 103)) 
jamesqiu
źródło
7

Warto, oto moja prosta implementacja funkcji zawiera dla list:

(defn list-contains? [coll value]
  (let [s (seq coll)]
    (if s
      (if (= (first s) value) true (recur (rest s) value))
      false)))
mikera
źródło
Czy możemy poprosić o część predykatu jako argument? Aby dostać coś takiego:(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
6

Jeśli masz wektor lub listę i chcesz sprawdzić, czy dana wartość jest zawarta w nim, okaże się, że contains?nie działa. Michał już wyjaśnił, dlaczego .

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

W tym przypadku możesz wypróbować cztery rzeczy:

  1. Zastanów się, czy naprawdę potrzebujesz wektora czy listy. Jeśli zamiast tego użyjesz zestawu , contains?zadziała.

    (contains? #{:a :b :c} :b) ; = true
  2. Użyjsome , owijając cel w zestaw w następujący sposób:

    (some #{:b} [:a :b :c]) ; = :b, which is truthy
  3. Skrót ustaw jako funkcję nie będzie działał, jeśli szukasz fałszywej wartości ( falselub nil).

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

    W takich przypadkach należy użyć wbudowanej funkcji predykatu dla tej wartości false?lub nil?:

    (some false? [true false true]) ; = true
  4. Jeśli będziesz musiał często przeprowadzać tego rodzaju wyszukiwanie, napisz dla niego funkcję :

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

Zobacz także odpowiedź Michała, aby dowiedzieć się, jak sprawdzić, czy któryś z wielu celów znajduje się w sekwencji.

Rory O'Kane
źródło
5

Oto szybka funkcja z moich standardowych narzędzi, których używam do tego celu:

(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))))
SOL__
źródło
Tak, twój ma tę zaletę, że zatrzyma się, gdy tylko znajdzie dopasowanie, zamiast kontynuować mapowanie całej sekwencji.
G__
5

Oto klasyczne rozwiązanie 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)))
Simon Brooke
źródło
4
OK, powodem, dla którego jest to kiepskie rozwiązanie w Clojure, jest to, że powtarza stos na jednym procesorze. Lepszym rozwiązaniem Clojure jest <pre> (defn member? [Elt col] (some # (= elt%) col)) </pre> Dzieje się tak, ponieważ somejest to potencjalnie równoległe dla dostępnych rdzeni.
Simon Brooke,
4

Zbudowałem na podstawie jg-faustus wersji „list-zawiera?”. Teraz wymaga dowolnej liczby argumentów.

(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))))
Urs Reupke
źródło
2

Jest to tak proste, jak użycie zestawu - podobnie jak mapy, możesz po prostu upuścić go w pozycji funkcji. Zwraca wartość if w zestawie (co jest prawdą) lub nil(co jest błędne):

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

Jeśli porównujesz wektor / listę o rozsądnych rozmiarach, których nie będziesz mieć przed uruchomieniem, możesz również użyć setfunkcji:

; (def nums '(100 101 102))
((set nums) 101) ; 101
Brad Koch
źródło
1

Zalecanym sposobem jest użycie somez zestawem - patrz dokumentacja clojure.core/some.

Możesz wtedy użyć somerzeczywistego predykatu prawda / fałsz, np

(defn in? [coll x] (if (some #{x} coll) true false))
KingCode
źródło
dlaczego if truei false? somejuż zwraca wartości prawda-ish i false-ish.
podsub
co z (jakieś # {nil} [nil])? Zwróciłoby nil, które zostanie zamienione na fałsz.
Wei Qiu
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))))))
David
źródło
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 )))

przykładowe użycie (które? [1 2 3] 3) lub (które? # {1 2 3} 4 5 3)

Michael
źródło
nadal nie ma funkcji dostarczonej przez rdzeń języka?
matanster
1

Ponieważ Clojure jest zbudowany na Javie, równie łatwo możesz wywołać funkcję .indexOfJava. Ta funkcja zwraca indeks dowolnego elementu w kolekcji, a jeśli nie może znaleźć tego elementu, zwraca -1.

Korzystając z tego moglibyśmy po prostu powiedzieć:

(not= (.indexOf [1 2 3 4] 3) -1)
=> true
AStanton
źródło
0

Problem z „zalecanym” rozwiązaniem polega na tym, że łamie się, gdy szukana wartość wynosi „zero”. Wolę takie rozwiązanie:

(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))
Simon Brooke
źródło
0

W bibliotece Tupelo dostępne są wygodne funkcje służące do tego celu . W szczególności, funkcje contains-elem?, contains-key?i contains-val?są bardzo przydatne. Pełna dokumentacja znajduje się w dokumentacji API .

contains-elem?jest najbardziej ogólny i jest przeznaczony dla wektorów lub innych klastrów 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))))

Tutaj widzimy, że dla zakresu liczb całkowitych lub wektora mieszanego contains-elem?działa zgodnie z oczekiwaniami zarówno dla istniejących, jak i nieistniejących elementów w kolekcji. W przypadku map możemy również wyszukać dowolną parę klucz-wartość (wyrażoną jako wektor 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] ))))

Przeszukiwanie zbioru jest również proste:

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

W przypadku map i zestawów prostsze (i wydajniejsze) jest użycie contains-key?do znalezienia wpisu mapy lub elementu zestawu:

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

W przypadku map możesz również wyszukiwać wartości za pomocą 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)))

Jak widać w teście, każda z tych funkcji działa poprawnie podczas wyszukiwania nilwartości.

Alan Thompson
źródło
0

Inna opcja:

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

Użyj java.util.Collection # zawiera ():

(.contains '(100 101 102) 101)
Alex
źródło