Testen Sie, ob eine Liste in Clojure einen bestimmten Wert enthält

161

Wie kann am besten getestet werden, ob eine Liste in Clojure einen bestimmten Wert enthält?

Insbesondere das Verhalten von contains?verwirrt mich derzeit:

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

Ich könnte natürlich eine einfache Funktion schreiben, um die Liste zu durchlaufen und auf Gleichheit zu testen, aber es muss sicher einen Standardweg geben, um dies zu tun?

mikera
quelle
7
Seltsam in der Tat enthält? muss die irreführendste Funktion in Clojure sein :) Hoffen wir, dass Clojure 1.3 sie in Contains-Key umbenannt bekommt? o.ä.
JG-Faustus
4
Ich denke, das wird jetzt mehrmals zu Tode geredet. enthält? wird sich nicht ändern. Siehe hier: groups.google.com/group/clojure/msg/f2585c149cd0465d und groups.google.com/group/clojure/msg/985478420223ecdf
kotarak
1
@ Kotarak danke für den Link! Ich stimme Rich hier tatsächlich in Bezug auf die Verwendung der enthaltenen zu? Name, obwohl ich denke, es sollte geändert werden, um einen Fehler zu werfen, wenn auf eine Liste oder Sequenz
angewendet

Antworten:

203

Ah, contains?... angeblich eine der fünf häufigsten FAQs zu: Clojure.

Es wird nicht geprüft, ob eine Sammlung einen Wert enthält. Es wird geprüft, ob ein Element mit abgerufen werden kann getoder mit anderen Worten, ob eine Sammlung einen Schlüssel enthält. Dies macht Sinn , für Sätze (die als zu machen keinen Unterschied zwischen Schlüssel und Werte betrachtet werden können), Karten (so (contains? {:foo 1} :foo)ist true) und Vektoren (aber beachten Sie, dass (contains? [:foo :bar] 0)isttrue , dass die Schlüssel hier Indizes sind und der betreffende Vektor die "enthält" Index 0!).

Um die Verwirrung zu verstärken, kehrt es in Fällen, in denen es keinen Sinn macht, anzurufen contains?, einfach zurück false. das ist es, was in (contains? :foo 1) und auch passiert (contains? '(100 101 102) 101). Aktualisieren: In Clojure wird ≥ 1,5 contains?geworfen, wenn ein Objekt eines Typs übergeben wird, der den beabsichtigten "Schlüsselmitgliedschaftstest" nicht unterstützt.

Der richtige Weg, um das zu tun, was Sie versuchen, ist wie folgt:

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

Bei der Suche nach einem von mehreren Artikeln können Sie einen größeren Satz verwenden. bei der Suche nach false/ nil, können Sie false?/ nil?- weil (#{x} x)Renditen x, so (#{nil} nil)ist nil; Wenn Sie nach einem von mehreren Elementen suchen, von denen einige möglicherweise falseoder sind nil, können Sie sie verwenden

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

(Beachten Sie, dass die Elemente zipmapin jeder Art von Sammlung weitergegeben werden können.)

Michał Marczyk
quelle
Danke Michal - du bist wie immer eine Schrift der Clojure-Weisheit! Sieht so aus, als würde ich in diesem Fall meine eigene Funktion schreiben ... es überrascht mich ein wenig, dass es noch keine in der Kernsprache gibt.
Mikera
4
Wie Michal sagte - es gibt bereits eine Funktion im Kern, die das tut, was Sie wollen: einige.
Kotarak
2
Oben kommentierte Michal (some #{101} '(100 101 102)), dass "dies meistens funktioniert". Ist es nicht fair zu sagen, dass es immer funktioniert? Ich verwende Clojure 1.4 und die Dokumentation verwendet diese Art von Beispiel. Es funktioniert für mich und macht Sinn. Gibt es einen Sonderfall, in dem es nicht funktioniert?
David J.
7
@DavidJames: Es funktioniert nicht, wenn Sie nach falseoder suchen nil- siehe folgenden Absatz. In Clojure 1.5-RC1 wird contains?eine Ausnahme ausgelöst, wenn eine nicht verschlüsselte Sammlung als Argument angegeben wird. Ich nehme an, ich werde diese Antwort bearbeiten, wenn die endgültige Version herauskommt.
Michał Marczyk
1
Das ist blöd! Der Hauptunterschied einer Sammlung ist die Zugehörigkeitsbeziehung. Es sollte die wichtigste Funktion für Sammlungen gewesen sein. en.wikipedia.org/wiki/Set_(mathematics)#Membership
jgomo3
132

Hier ist mein Standard-Util für den gleichen Zweck:

(defn in? 
  "true if coll contains elm"
  [coll elm]  
  (some #(= elm %) coll))
jg-faustus
quelle
36
Dies ist die einfachste und sicherste Lösung, da sie auch falsche Werte wie nilund verarbeitet false. Warum ist das nicht Teil von Clojure / Core?
Stian Soiland-Reyes
2
seqkönnte vielleicht umbenannt werden coll, um Verwechslungen mit der Funktion zu vermeiden seq?
nha
3
@nha Das könntest du machen, ja. Hier spielt es keine Rolle: Da wir die Funktion seqim Körper nicht verwenden, besteht kein Konflikt mit dem gleichnamigen Parameter. Sie können die Antwort jedoch jederzeit bearbeiten, wenn Sie der Meinung sind, dass das Umbenennen das Verständnis erleichtern würde.
JG-Faustus
1
Es ist erwähnenswert, dass dies 3-4x langsamer sein kann, als (boolean (some #{elm} coll))wenn Sie sich keine Sorgen machen müssen niloder false.
Neverfox
2
@AviFlax Ich habe über clojure.org/guides/threading_macros nachgedacht , wo es heißt: "Konventionell erwarten Kernfunktionen, die mit Sequenzen arbeiten, die Sequenz als letztes Argument. Dementsprechend enthalten Pipelines, die Map, Filter, Entfernen, Reduzieren, In usw. Enthalten Rufen Sie normalerweise das Makro - >> auf. " Aber ich denke, die Konvention handelt mehr von Funktionen, die mit Sequenzen arbeiten und Sequenzen zurückgeben.
John Wiseman
18

Sie können Java-Methoden jederzeit mit der Syntax .methodName aufrufen.

(.contains [100 101 102] 101) => true
Yury Litvinov
quelle
5
IMHO ist dies die beste Antwort. Schade, dass Clojure enthält? ist so verwirrend benannt.
Mikkom
1
Der ehrwürdige Meister Qc Na ging mit seinem Schüler Anton spazieren. Als Anton ihm von einem Anfängerproblem erzählte contains?, schlug Qc Na ihn mit einem Bô und sagte: "Blöder Schüler! Man muss erkennen, dass es keinen Löffel gibt. Es ist alles nur Java darunter! Verwenden Sie die Punktnotation." In diesem Moment wurde Anton erleuchtet.
David Tonhofer
16

Ich weiß, dass ich etwas spät dran bin, aber was ist mit:

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

Endlich in Clojure 1.4 gibt true aus :)

Giuliani Deon
quelle
3
(set '(101 102 103))ist das gleiche wie %{101 102 103}. Ihre Antwort kann also wie folgt geschrieben werden (contains? #{101 102 103} 102).
David J.
4
Dies hat den Nachteil, dass die Konvertierung der ursprünglichen Liste '(101 102 103)in einen Satz erforderlich ist .
David J.
12
(not= -1 (.indexOf '(101 102 103) 102))

Funktioniert, aber unten ist besser:

(some #(= 102 %) '(101 102 103)) 
Jamesqiu
quelle
7

Für das, was es wert ist, ist dies meine einfache Implementierung einer Include-Funktion für Listen:

(defn list-contains? [coll value]
  (let [s (seq coll)]
    (if s
      (if (= (first s) value) true (recur (rest s) value))
      false)))
mikera
quelle
Können wir nach dem Prädikatteil als Argument fragen? Um etwas zu bekommen wie:(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

Wenn Sie einen Vektor oder eine Liste haben und überprüfen möchten, ob ein Wert darin enthalten ist, werden Sie feststellen, dass contains?dies nicht funktioniert. Michał hat bereits erklärt warum .

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

In diesem Fall können Sie vier Dinge ausprobieren:

  1. Überlegen Sie, ob Sie wirklich einen Vektor oder eine Liste benötigen. Wenn Sie stattdessen ein Set verwenden , contains?funktioniert dies.

    (contains? #{:a :b :c} :b) ; = true
  2. Verwenden Siesome das Ziel wie folgt in einem Satz:

    (some #{:b} [:a :b :c]) ; = :b, which is truthy
  3. Die Verknüpfung zum Festlegen der Funktion funktioniert nicht, wenn Sie nach einem falschen Wert ( falseoder nil) suchen .

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

    In diesen Fällen sollten Sie die integrierte Prädikatfunktion für diesen Wert verwenden, false?oder nil?:

    (some false? [true false true]) ; = true
  4. Wenn Sie diese Art der Suche häufig durchführen müssen, schreiben Sie eine Funktion dafür :

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

In Michałs Antwort finden Sie auch Möglichkeiten, um zu überprüfen, ob mehrere Ziele in einer Sequenz enthalten sind.

Rory O'Kane
quelle
5

Hier ist eine kurze Funktion meiner Standarddienstprogramme, die ich für diesen Zweck verwende:

(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__
quelle
Ja, Ihre hat den Vorteil, dass sie beendet wird, sobald sie eine Übereinstimmung findet, anstatt die gesamte Sequenz weiter abzubilden.
G__
5

Hier ist die klassische Lisp-Lösung:

(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
quelle
4
OK, der Grund für die schlechte Lösung in Clojure ist, dass der Stapel auf einem Prozessor rekursiv ist. Eine bessere Clojure-Lösung ist <pre> (defn member? [Elt col] (einige # (= elt%) col)) </ pre> Dies liegt daran, dass die someverfügbaren Kerne möglicherweise parallel sind.
Simon Brooke
4

Ich habe auf der jg-faustus- Version von "list-enthält?" Es sind jetzt beliebig viele Argumente erforderlich.

(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
quelle
2

Es ist so einfach wie die Verwendung eines Sets - ähnlich wie bei Karten können Sie es einfach an der Funktionsposition ablegen. Es wird als Wert ausgewertet, wenn in der Menge (was wahr ist) oder nil(was falsch ist):

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

Wenn Sie mit einem Vektor / einer Liste mit angemessener Größe vergleichen, die Sie erst zur Laufzeit haben, können Sie auch die folgende setFunktion verwenden:

; (def nums '(100 101 102))
((set nums) 101) ; 101
Brad Koch
quelle
1

Die empfohlene Methode ist die Verwendung somemit einem Set - siehe Dokumentation zuclojure.core/some .

Sie können dann someinnerhalb eines echten wahr / falsch-Prädikats verwenden, z

(defn in? [coll x] (if (some #{x} coll) true false))
KingCode
quelle
warum das if trueund false? somegibt bereits true-ish- und false-ish-Werte zurück.
Subsub
was ist mit (etwas # {nil} [nil])? Es würde null zurückgeben, was in false konvertiert wird.
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
quelle
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 )))

Beispielverwendung (welche? [1 2 3] 3) oder (welche? # {1 2 3} 4 5 3)

Michael
quelle
noch keine sprachkernfunktion dafür geliefert?
Matanster
1

Da Clojure auf Java basiert, können Sie das genauso einfach aufrufen .indexOf Java-Funktion . Diese Funktion gibt den Index eines Elements in einer Auflistung zurück. Wenn dieses Element nicht gefunden werden kann, wird -1 zurückgegeben.

Wenn wir dies nutzen, können wir einfach sagen:

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

Das Problem mit der "empfohlenen" Lösung ist, dass es zu Unterbrechungen kommt, wenn der gesuchte Wert "Null" ist. Ich bevorzuge diese Lösung:

(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
quelle
0

Zu diesem Zweck gibt es in der Tupelo-Bibliothek praktische Funktionen . Insbesondere sind die Funktionen contains-elem?, contains-key?und contains-val?sind sehr nützlich. Die vollständige Dokumentation finden Sie in den API-Dokumenten .

contains-elem?ist die allgemeinste und ist für Vektoren oder andere Clojure gedacht 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))))

Hier sehen wir, dass für einen ganzzahligen Bereich oder einen gemischten Vektor contains-elem?sowohl für vorhandene als auch für nicht vorhandene Elemente in der Sammlung wie erwartet funktioniert. Für Karten können wir auch nach einem beliebigen Schlüssel-Wert-Paar suchen (ausgedrückt als len-2-Vektor):

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

Es ist auch einfach, einen Satz zu suchen:

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

Bei Karten und Sets ist es einfacher (und effizienter), contains-key?einen Karteneintrag oder ein Set-Element zu finden:

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

Für Karten können Sie auch nach Werten suchen mit 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)))

Wie im Test zu sehen ist, funktioniert jede dieser Funktionen bei der Suche nach nilWerten ordnungsgemäß .

Alan Thompson
quelle
0

Andere Option:

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

Verwenden Sie java.util.Collection # enthält ():

(.contains '(100 101 102) 101)
Alex
quelle