Verifica se un elenco contiene un valore specifico in Clojure


163

Qual è il modo migliore per verificare se un elenco contiene un determinato valore in Clojure?

In particolare, il comportamento di contains?attualmente mi sta confondendo:

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

Potrei ovviamente scrivere una semplice funzione per attraversare l'elenco e verificare l'uguaglianza, ma deve esserci sicuramente un modo standard per farlo?


7
Strano davvero, contiene? deve essere la funzione più fuorviante in Clojure :) Speriamo che Clojure 1.3 la veda rinominata in chiave-contiene? o simili.
jg-faustus,

4
Penso che si parli più volte di morte. contiene? non cambierà. Vedi qui: groups.google.com/group/clojure/msg/f2585c149cd0465d and groups.google.com/group/clojure/msg/985478420223ecdf
kotarak

1
@kotarak grazie per il link! In realtà sono d'accordo con Rich qui in termini di utilizzo dei contenuti? nome anche se penso che dovrebbe essere modificato per generare un errore quando applicato a un elenco o una sequenza
mikera

Risposte:


204

Ah, contains?... presumibilmente una delle cinque principali domande frequenti: Clojure.

Esso non controlla se una collezione contiene un valore; controlla se un elemento può essere recuperato con geto, in altre parole, se una raccolta contiene una chiave. Questo ha senso per insiemi (che si può pensare che non facciano alcuna distinzione tra chiavi e valori), mappe (così (contains? {:foo 1} :foo)è true) e vettori (ma si noti che (contains? [:foo :bar] 0)è trueperché le chiavi qui sono indici e il vettore in questione "contiene" il indice 0!).

Da aggiungere alla confusione, nei casi in cui non ha senso chiamare contains?, semplicemente ritorna false; questo è ciò che accade dentro (contains? :foo 1) e anche (contains? '(100 101 102) 101) . Aggiornamento: In Clojure ≥ 1,5 contains?lancia quando viene consegnato un oggetto di un tipo che non supporta il test di "appartenenza chiave" previsto.

Il modo corretto di fare quello che stai cercando di fare è il seguente:

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

Quando cerchi uno di un gruppo di oggetti, puoi usare un set più grande; durante la ricerca di false/ nil, è possibile utilizzare false?/ nil?- poiché (#{x} x)restituisce x, quindi (#{nil} nil)è nil; durante la ricerca di uno o più elementi, alcuni dei quali possono essere falseo nil, è possibile utilizzare

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

(Si noti che gli articoli possono essere passati zipmapin qualsiasi tipo di raccolta.)


Grazie Michal - sei un fonte di saggezza Clojure come sempre! Sembra che scriverò la mia funzione in questo caso ... mi sorprende leggermente che non ce ne sia già una nella lingua principale.
Mikera,

4
Come ha detto Michal, nel nucleo c'è già una funzione che fa ciò che desideri: alcuni.
Kotarak,

2
Sopra, Michal ha commentato (some #{101} '(100 101 102))dicendo che "il più delle volte funziona". Non è giusto dire che funziona sempre? Sto usando Clojure 1.4 e la documentazione usa questo tipo di esempio. Funziona per me e ha senso. Esiste una specie di caso speciale in cui non funziona?
David J.

7
@DavidJames: non funziona se stai verificando la presenza di falseo nil- vedi il paragrafo seguente. In una nota a parte, in Clojure 1.5-RC1 contains?genera un'eccezione quando viene data una raccolta senza chiave come argomento. Suppongo che modificherò questa risposta quando uscirà la versione finale.
Michał Marczyk,

1
Questo è stupido! La principale distinzione di una raccolta è la relazione di appartenenza. Avrebbe dovuto essere la funzione più importante per le collezioni. it.wikipedia.org/wiki/Set_(mathematics)#Membership
jgomo3

132

Ecco il mio util standard per lo stesso scopo:

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

36
Questa è la soluzione più semplice e sicura, poiché gestisce anche valori falsi come nile false. Ora, perché questo non fa parte del clojure / core?
Stian Soiland-Reyes

2
seqpotrebbe essere rinominato coll, per evitare confusione con la funzione seq?
nha

3
@nha Potresti farlo, sì. Non importa qui: dal momento che non stiamo usando la funzione seqall'interno del corpo, non c'è conflitto con il parametro con lo stesso nome. Sentiti libero di modificare la risposta se ritieni che la ridenominazione faciliterebbe la comprensione.
jg-faustus,

1
Vale la pena notare che questo può 3-4 volte più lento rispetto a (boolean (some #{elm} coll))se non devi preoccuparti di nilo false.
Neverfox,

2
@AviFlax Stavo pensando a clojure.org/guides/threading_macros , dove si dice "Per convenzione, le funzioni principali che operano sulle sequenze si aspettano la sequenza come ultimo argomento. Di conseguenza, pipeline contenenti map, filter, remove, riduc, into, etc di solito richiede la macro - >> ". Ma immagino che la convenzione riguardi più le funzioni che operano su sequenze e sequenze di ritorno.
John Wiseman,

18

Puoi sempre chiamare i metodi java con la sintassi .methodName.

(.contains [100 101 102] 101) => true

5
IMHO questa è la risposta migliore. Troppo cattivo clojure contiene? è così confuso.
Mikkom,

1
Il venerabile maestro Qc Na stava camminando con il suo studente, Anton. Quando Anton gli disse di avere un problema con un principiante contains?, Qc Na lo colpì con un Bô e gli disse: "Stupido studente! Devi capire che non c'è un cucchiaio. È tutto solo Java sotto! Usa la notazione punto". In quel momento, Anton si illuminò.
David Tonhofer,

17

So di essere un po 'in ritardo, ma che dire di:

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

Finalmente nel clojure 1.4 le uscite sono vere :)


3
(set '(101 102 103))è lo stesso di %{101 102 103}. Quindi la tua risposta può essere scritta come (contains? #{101 102 103} 102).
David J.

4
Ciò ha lo svantaggio di richiedere la conversione dell'elenco originale '(101 102 103)in un set.
David J.

12
(not= -1 (.indexOf '(101 102 103) 102))

Funziona, ma di seguito è meglio:

(some #(= 102 %) '(101 102 103)) 

7

Per quello che vale, questa è la mia semplice implementazione di una funzione contiene per gli elenchi:

(defn list-contains? [coll value]
  (let [s (seq coll)]
    (if s
      (if (= (first s) value) true (recur (rest s) value))
      false)))

Possiamo chiedere la parte predicato come argomento? Per ottenere qualcosa del genere:(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

Se hai un vettore o un elenco e desideri verificare se un valore è contenuto in esso, scoprirai che contains?non funziona. Michał ha già spiegato il perché .

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

Ci sono quattro cose che puoi provare in questo caso:

  1. Valuta se hai davvero bisogno di un vettore o di un elenco. Se invece usi un set , contains?funzionerà.

    (contains? #{:a :b :c} :b) ; = true
  2. Utilizzaresome , avvolgendo il target in un set, come segue:

    (some #{:b} [:a :b :c]) ; = :b, which is truthy
  3. Il collegamento impostato come funzione non funzionerà se si sta cercando un valore errato ( falseo nil).

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

    In questi casi, è necessario utilizzare la funzione predicato integrata per quel valore false?oppure nil?:

    (some false? [true false true]) ; = true
  4. Se devi fare molto questo tipo di ricerca, scrivi una funzione per questo :

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

Inoltre, vedi la risposta di Michał per i modi per verificare se uno o più target sono contenuti in una sequenza.


5

Ecco una rapida funzione delle mie utility standard che utilizzo a questo scopo:

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

Sì, il tuo ha il vantaggio di fermarsi non appena trova una corrispondenza piuttosto che continuare a mappare l'intera sequenza.
G__

5

Ecco la classica soluzione 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
OK, la ragione per cui è una cattiva soluzione in Clojure è che recluta lo stack su un processore. Una soluzione Clojure migliore è <pre> (defn member? [Elt col] (some # (= elt%) col)) </pre> Questo perché someè potenzialmente parallelo tra i core disponibili.
Simon Brooke,

4

Ho costruito sulla versione jg-faustus di "list-contiene?". Ora accetta un numero qualsiasi di argomenti.

(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

È semplice come usare un set - simile alle mappe, puoi semplicemente rilasciarlo nella posizione della funzione. Valuta il valore se nell'insieme (che è vero) o nil(che è falso):

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

Se stai confrontando un vettore / elenco di dimensioni ragionevoli che non avrai fino al runtime, puoi anche usare la setfunzione:

; (def nums '(100 101 102))
((set nums) 101) ; 101

1

Il modo consigliato è utilizzare somecon un set - consultare la documentazione per clojure.core/some.

È quindi possibile utilizzare someall'interno di un vero predicato vero / falso, ad es

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

perché il if truee false? somerestituisce già i valori true-ish e false-ish.
subsub

che dire di (alcuni # {nil} [nil])? Restituirà zero, che verrà convertito in falso.
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))))))

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

esempio di utilizzo (quale? [1 2 3] 3) o (quale? # {1 2 3} 4 5 3)


ancora nessuna funzione fornita da language-core per questo?
matanster

1

Poiché Clojure è basato su Java, è possibile chiamare facilmente anche la .indexOffunzione Java. Questa funzione restituisce l'indice di qualsiasi elemento in una raccolta e, se non riesce a trovare questo elemento, restituisce -1.

Facendo uso di questo potremmo semplicemente dire:

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

0

Il problema con la soluzione 'consigliata' è che si rompe quando il valore che stai cercando è 'zero'. Preferisco questa soluzione:

(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

Ci sono utili funzioni a questo scopo nella libreria Tupelo . In particolare, le funzioni contains-elem?, contains-key?e contains-val?sono molto utili. La documentazione completa è presente nei documenti API .

contains-elem?è il più generico ed è inteso per vettori o qualsiasi altro 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))))

Qui vediamo che per un intervallo intero o un vettore misto, contains-elem?funziona come previsto per gli elementi esistenti e inesistenti nella raccolta. Per le mappe, possiamo anche cercare qualsiasi coppia chiave-valore (espressa come vettore 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] ))))

È anche semplice cercare un set:

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

Per mappe e set, è più semplice (e più efficiente) utilizzare contains-key?per trovare una voce della mappa o un elemento set:

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

E, per le mappe, puoi anche cercare valori con 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)))

Come visto nel test, ciascuna di queste funzioni funziona correttamente quando si cercano nilvalori.


0

Un'altra opzione:

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

Usa java.util.Collection # Includes ():

(.contains '(100 101 102) 101)
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.