Tester si une liste contient une valeur spécifique dans Clojure

163

Quelle est la meilleure façon de tester si une liste contient une valeur donnée dans Clojure?

En particulier, le comportement de contains?me déroute actuellement:

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

Je pourrais évidemment écrire une fonction simple pour parcourir la liste et tester l'égalité, mais il doit sûrement y avoir un moyen standard de le faire?

Mikera
la source
7
Etrange en effet, contient? doit être la fonction nommée la plus trompeuse de Clojure :) En espérant que Clojure 1.3 la verra renommée en contains-key? ou similaire.
jg-faustus
4
Je pense que cela est dit à mort plusieurs fois maintenant. contient? ne changera pas. Voir ici: groups.google.com/group/clojure/msg/f2585c149cd0465d et groups.google.com/group/clojure/msg/985478420223ecdf
kotarak
1
@kotarak merci pour le lien! Je suis vraiment d'accord avec Rich ici en ce qui concerne l'utilisation du contient? nom bien que je pense qu'il devrait être modifié pour lancer une erreur lorsqu'il est appliqué à une liste ou à une séquence
mikera

Réponses:

204

Ah, contains?... soi-disant l'une des cinq principales FAQ concernant Clojure.

Il ne vérifie pas si une collection contient une valeur; il vérifie si un élément peut être récupéré avec getou, en d'autres termes, si une collection contient une clé. Cela a du sens pour les ensembles (qui peuvent être considérés comme ne faisant aucune distinction entre les clés et les valeurs), les cartes (ainsi (contains? {:foo 1} :foo)est true) et les vecteurs (mais notez que (contains? [:foo :bar] 0)c'est true, parce que les clés ici sont des indices et le vecteur en question "contient" le index 0!).

Pour ajouter à la confusion, dans les cas où cela n'a pas de sens d'appeler contains?, il revient simplement false; c'est ce qui se passe dans (contains? :foo 1) et aussi (contains? '(100 101 102) 101) . Mise à jour: Dans Clojure ≥ 1.5 contains?lance quand on lui donne un objet d'un type qui ne prend pas en charge le test "d'appartenance clé" prévu.

La bonne façon de faire ce que vous essayez de faire est la suivante:

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

Lorsque vous recherchez l'un des nombreux éléments, vous pouvez utiliser un ensemble plus grand; lors de la recherche false/ nil, vous pouvez utiliser false?/ nil?- parce que les (#{x} x)rendements x, ce qui (#{nil} nil)est nil; lors de la recherche d'un élément parmi plusieurs, dont certains peuvent être falseou nil, vous pouvez utiliser

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

(Notez que les éléments peuvent être transmis zipmapdans n'importe quel type de collection.)

Michał Marczyk
la source
Merci Michal - vous êtes une source de sagesse Clojure comme toujours! On dirait que je vais écrire ma propre fonction dans ce cas ... cela me surprend un peu qu'il n'y en ait pas déjà une dans le langage de base.
mikera
4
Comme l'a dit Michal - il y a déjà une fonction dans le noyau qui fait ce que vous désirez: certaines.
kotarak
2
Ci-dessus, Michal a commenté en (some #{101} '(100 101 102))disant que "la plupart du temps cela fonctionne". N'est-il pas juste de dire que cela fonctionne toujours? J'utilise Clojure 1.4 et la documentation utilise ce genre d'exemple. Cela fonctionne pour moi et a du sens. Y a-t-il une sorte de cas particulier où cela ne fonctionne pas?
David J.
7
@DavidJames: Cela ne fonctionne pas si vous vérifiez la présence de falseou nil- voir le paragraphe suivant. Sur une note distincte, dans Clojure 1.5-RC1 contains?lève une exception lorsqu'on lui donne une collection sans clé comme argument. Je suppose que je modifierai cette réponse lorsque la version finale sortira.
Michał Marczyk
1
C'est stupide! La principale distinction d'une collection est la relation d'appartenance. Cela aurait dû être la fonction la plus importante des collections. en.wikipedia.org/wiki/Set_(mathematics)#Membership
jgomo3
132

Voici mon utilitaire standard dans le même but:

(defn in? 
  "true if coll contains elm"
  [coll elm]  
  (some #(= elm %) coll))
jg-faustus
la source
36
C'est la solution la plus simple et la plus sûre, car elle gère également des valeurs fausses telles que nilet false. Maintenant, pourquoi ne fait-il pas partie de clojure / core?
Stian Soiland-Reyes
2
seqpourrait peut-être être renommé en coll, pour éviter toute confusion avec la fonction seq?
nha
3
@nha Vous pouvez faire ça, oui. Cela n'a pas d'importance ici: puisque nous n'utilisons pas la fonction seqà l'intérieur du corps, il n'y a pas de conflit avec le paramètre du même nom. Mais n'hésitez pas à modifier la réponse si vous pensez que le changement de nom faciliterait la compréhension.
jg-faustus
1
Il convient de noter que cela peut 3 à 4 fois plus lentement que (boolean (some #{elm} coll))si vous n'avez pas à vous soucier de nilou false.
neverfox
2
@AviFlax Je pensais à clojure.org/guides/threading_macros , où il est dit "Par convention, les fonctions de base qui opèrent sur des séquences attendent la séquence comme leur dernier argument. Par conséquent, les pipelines contenant mapper, filtrer, supprimer, réduire, en, etc. appelle généralement la macro - >>. " Mais je suppose que la convention concerne davantage les fonctions qui opèrent sur des séquences et des séquences de retour.
John Wiseman
18

Vous pouvez toujours appeler des méthodes java avec la syntaxe .methodName.

(.contains [100 101 102] 101) => true
Yury Litvinov
la source
5
IMHO c'est la meilleure réponse. Trop mauvais clojure contient? est nommé si confus.
mikkom
1
Le vénérable maître Qc Na marchait avec son élève, Anton. Quand Anton lui a dit qu'il avait un problème avec un débutant contains?, Qc Na l'a frappé avec un Bô et a dit: "Stupide étudiant! Vous devez réaliser qu'il n'y a pas de cuillère. C'est juste Java en dessous! Utilisez la notation par points.". À ce moment-là, Anton s'est éclairé.
David Tonhofer
17

Je sais que je suis un peu en retard, mais qu'en est-il:

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

Enfin dans clojure 1.4 sort vrai :)

Giuliani Deon
la source
3
(set '(101 102 103))est le même que %{101 102 103}. Donc, votre réponse peut être écrite comme (contains? #{101 102 103} 102).
David J.
4
Cela présente l'inconvénient de nécessiter la conversion de la liste d'origine '(101 102 103)en un ensemble.
David J.
12
(not= -1 (.indexOf '(101 102 103) 102))

Fonctionne, mais ci-dessous est mieux:

(some #(= 102 %) '(101 102 103)) 
jamesqiu
la source
7

Pour ce que cela vaut, voici ma simple implémentation d'une fonction contient pour les listes:

(defn list-contains? [coll value]
  (let [s (seq coll)]
    (if s
      (if (= (first s) value) true (recur (rest s) value))
      false)))
Mikera
la source
Pouvons-nous demander la partie prédicat comme argument? Pour obtenir quelque chose comme:(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

Si vous avez un vecteur ou une liste et que vous souhaitez vérifier si une valeur y est contenue, vous constaterez que contains?cela ne fonctionne pas. Michał a déjà expliqué pourquoi .

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

Il y a quatre choses que vous pouvez essayer dans ce cas:

  1. Demandez-vous si vous avez vraiment besoin d'un vecteur ou d'une liste. Si vous utilisez un ensemble à la place , contains?cela fonctionnera.

    (contains? #{:a :b :c} :b) ; = true
  2. Utilisezsome , enveloppant la cible dans un ensemble, comme suit:

    (some #{:b} [:a :b :c]) ; = :b, which is truthy
  3. Le raccourci définir en tant que fonction ne fonctionnera pas si vous recherchez une valeur erronée ( falseou nil).

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

    Dans ces cas, vous devez utiliser la fonction de prédicat intégrée pour cette valeur, false?ou nil?:

    (some false? [true false true]) ; = true
  4. Si vous devez souvent effectuer ce type de recherche, écrivez une fonction pour cela :

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

Voir également la réponse de Michał pour savoir si l'une des cibles multiples est contenue dans une séquence.

Rory O'Kane
la source
5

Voici une fonction rapide de mes utilitaires standard que j'utilise à cet effet:

(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__
la source
Oui, le vôtre a l'avantage qu'il s'arrêtera dès qu'il trouvera une correspondance plutôt que de continuer à cartographier toute la séquence.
G__
5

Voici la solution Lisp classique:

(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
la source
4
OK, la raison pour laquelle cette solution est médiocre dans Clojure est qu'elle réintègre la pile sur un processeur. Une meilleure solution Clojure est <pre> (defn member? [Elt col] (some # (= elt%) col)) </pre> C'est parce qu'il someest potentiellement parallèle entre les cœurs disponibles.
Simon Brooke
4

J'ai construit sur la version jg-faustus de "list-contains?". Il prend maintenant n'importe quel nombre d'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))))
Urs Reupke
la source
2

C'est aussi simple que d'utiliser un ensemble - semblable aux cartes, vous pouvez simplement le déposer dans la position de fonction. Il évalue à la valeur si dans l'ensemble (qui est vrai) ou nil(qui est faux):

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

Si vous comparez un vecteur / une liste de taille raisonnable que vous n'aurez pas avant l'exécution, vous pouvez également utiliser la setfonction:

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

La méthode recommandée est de l'utiliser someavec un ensemble - voir la documentation pour clojure.core/some.

Vous pouvez alors utiliser somedans un vrai prédicat vrai / faux, par exemple

(defn in? [coll x] (if (some #{x} coll) true false))
KingCode
la source
pourquoi le if trueet false? somerenvoie déjà des valeurs true-ish et false-ish.
sous
qu'en est-il (certains # {nil} [nil])? Il renverrait nil qui sera converti en faux.
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
la source
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 )))

exemple d'utilisation (laquelle? [1 2 3] 3) ou (laquelle? # {1 2 3} 4 5 3)

Michael
la source
toujours pas de fonction fournie par le noyau du langage?
matanster
1

Puisque Clojure est construit sur Java, vous pouvez tout aussi facilement appeler la .indexOffonction Java. Cette fonction renvoie l'index de tout élément d'une collection et, si elle ne trouve pas cet élément, renvoie -1.

En utilisant cela, nous pourrions simplement dire:

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

Le problème avec la solution «recommandée» est qu'elle est interrompue lorsque la valeur que vous recherchez est «nulle». Je préfère cette solution:

(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
la source
0

Il existe des fonctions pratiques à cet effet dans la bibliothèque Tupelo . En particulier, les fonctions contains-elem?, contains-key?et contains-val?sont très utiles. Une documentation complète est présente dans la documentation de l'API .

contains-elem?est le plus générique et est destiné aux vecteurs ou à tout autre 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))))

Ici, nous voyons que pour une plage d'entiers ou un vecteur mixte, contains-elem?fonctionne comme prévu pour les éléments existants et inexistants dans la collection. Pour les cartes, nous pouvons également rechercher n'importe quelle paire clé-valeur (exprimée sous forme de vecteur 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] ))))

Il est également simple de rechercher un ensemble:

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

Pour les cartes et les ensembles, il est plus simple (et plus efficace) d'utiliser contains-key?pour trouver une entrée de carte ou un élément d'ensemble:

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

Et, pour les cartes, vous pouvez également rechercher des valeurs avec 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)))

Comme vu dans le test, chacune de ces fonctions fonctionne correctement lors de la recherche de nilvaleurs.

Alan Thompson
la source
0

Une autre option:

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

Utilisez java.util.Collection # contains ():

(.contains '(100 101 102) 101)
Alex
la source