Quel est le meilleur moyen de vérifier si une liste contient une valeur donnée dans Clojure?
En particulier, le comportement de contains?
m'embrouille 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?
Ah, contains?
... soi-disant l’une des cinq questions les plus fréquemment posées 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 get
ou, en d'autres termes, si une collection contient une clé. Cela a du sens pour les ensembles (ce qui peut être considéré comme ne faisant aucune distinction entre les clés et les valeurs), les cartes (donc (contains? {:foo 1} :foo)
est true
) et des vecteurs (mais notez que (contains? [:foo :bar] 0)
est true
, car les clés ici sont des index et le vecteur en question "contient" l’index 0
!).
Pour ajouter à la confusion, dans les cas où il n’a pas de sens d’appeler Mise à jour: Dans Clojure ≥ 1.5 contains?
, il retourne simplement false
; c'est ce qui se passe dans (contains? :foo 1)
et aussi (contains? '(100 101 102) 101)
.contains?
lance un objet d'un type qui ne prend pas en charge le test "d'appartenance à une 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 objets, vous pouvez utiliser un plus grand ensemble. lors de la recherche de false
/nil
, vous pouvez utiliser false?
/nil?
-- car (#{x} x)
retourne x
, donc (#{nil} nil)
est nil
; lors de la recherche d’un ou plusieurs éléments dont certains peuvent être false
ou nil
, vous pouvez utiliser
(some (zipmap [...the items...] (repeat true)) the-collection)
(Notez que les éléments peuvent être passés à zipmap
dans n'importe quel type de collection.)
Voici mon util standard pour le même but:
(defn in?
"true if coll contains Elm"
[coll Elm]
(some #(= Elm %) coll))
Je sais que je suis un peu en retard, mais qu'en est-il:
(contains? (set '(101 102 103)) 102)
Enfin, dans la version 1.4, les sorties sont vraies :)
Vous pouvez toujours appeler les méthodes Java avec la syntaxe .methodName.
(.contains [100 101 102] 101) => true
(not= -1 (.indexOf '(101 102 103) 102))
Fonctionne, mais en dessous c'est mieux:
(some #(= 102 %) '(101 102 103))
Pour ce qu’il vaut, c’est 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)))
Si vous avez un vecteur ou une liste et que vous voulez vérifier si valeur est contenu dans celui-ci, vous constaterez que contains?
ne fonctionne pas. Michał a déjà expliqué pourquoi .
; does not work as you might expect
(contains? [:a :b :c] :b) ; = false
Dans ce cas, vous pouvez essayer quatre choses:
Déterminez si vous avez vraiment besoin d’un vecteur ou d’une liste. Si vous utilisez un ensemble à la place , contains?
marchera.
(contains? #{:a :b :c} :b) ; = true
Utilisez some
, en enveloppant la cible dans un ensemble, comme suit:
(some #{:b} [:a :b :c]) ; = :b, which is truthy
Le raccourci set-as-function ne fonctionnera pas si vous recherchez une valeur de fausseté (false
ou nil
).
; will not work
(some #{false} [true false true]) ; = nil
Dans ces cas, vous devriez utiliser la fonction de prédicat intégrée pour cette valeur, false?
ou nil?
:
(some false? [true false true]) ; = true
Si vous devez beaucoup faire ce genre de recherche, écrivez une fonction pour :
(defn seq-contains? [coll target] (some #(= target %) coll))
(seq-contains? [true false true] false) ; = true
Voir aussi réponse de Michał pour savoir comment vérifier si des cibles multiples sont contenues dans une séquence.
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)))
Voici une rapide fonction de mes utilitaires standard que j'utilise à cette fin:
(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))))
J'ai construit sur j-g-faustus version de "list-contient?". Il faut maintenant un nombre quelconque 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))))
C'est aussi simple que d'utiliser un ensemble - semblable aux cartes, vous pouvez simplement le déposer à la position de la fonction. Il évalue à la valeur si dans l'ensemble (ce qui est la vérité) ou nil
(ce qui est falsey):
(#{100 101 102} 101) ; 101
(#{100 101 102} 99) ; nil
Si vous vérifiez par rapport à un vecteur/une liste de taille raisonnable que vous n’aurez qu’à l’exécution, vous pouvez également utiliser la fonction set
:
; (def nums '(100 101 102))
((set nums) 101) ; 101
(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 (lequel? [1 2 3] 3) ou (quel? # {1 2 3} 4 5 3)
(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))))))
La méthode recommandée consiste à utiliser some
avec un ensemble - voir la documentation de clojure.core/some
.
Vous pouvez ensuite utiliser some
dans un vrai prédicat vrai/faux, par exemple.
(defn in? [coll x] (if (some #{x} coll) true false))
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))))
Nous voyons ici que pour une plage entière ou un vecteur mixte, contains-elem?
fonctionne comme prévu pour les éléments existants et non existants de la collection. Pour les cartes, nous pouvons également rechercher n’importe quelle paire clé-valeur (exprimée sous la forme d’un 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 de jeu:
(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 indiqué dans le test, chacune de ces fonctions fonctionne correctement lors de la recherche de valeurs nil
.
Le problème avec la solution "recommandée" est qu’il s’agit d’une rupture lorsque la valeur recherchée 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))
Clojure étant basé sur Java, vous pouvez tout aussi facilement appeler le .indexOf
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