Pruebe si una lista contiene un valor específico en Clojure

163

¿Cuál es la mejor manera de probar si una lista contiene un valor dado en Clojure?

En particular, el comportamiento de contains?me está confundiendo actualmente:

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

Obviamente, podría escribir una función simple para recorrer la lista y probar la igualdad, pero seguramente debe haber una forma estándar de hacerlo.

mikera
fuente
77
Extraño de hecho, contiene? tiene que ser la función con el nombre más engañoso en Clojure :) ¿Esperamos que Clojure 1.3 lo renombre como contiene-clave? o similar.
jg-faustus
44
Creo que esto se habla con la muerte varias veces ahora. contiene? no cambiará. Ver aquí: groups.google.com/group/clojure/msg/f2585c149cd0465d y groups.google.com/group/clojure/msg/985478420223ecdf
kotarak el
1
@kotarak gracias por el enlace! ¿Estoy de acuerdo con Rich aquí en términos del uso de los contenidos? aunque creo que debería modificarse para arrojar un error cuando se aplica a una lista o secuencia
mikera

Respuestas:

204

Ah, contains?... supuestamente una de las cinco preguntas más frecuentes sobre re: Clojure.

No , no comprueba si una colección contiene un valor; comprueba si un elemento se puede recuperar con geto, en otras palabras, si una colección contiene una clave. Esto tiene sentido para los conjuntos (que puede ser pensado como no haciendo distinción entre claves y valores), mapas (por lo que (contains? {:foo 1} :foo)es true) y vectores (pero nota que (contains? [:foo :bar] 0)es true, porque las claves aquí son índices y el vector en cuestión no "contienen" la índice 0!).

Para agregar a la confusión, en los casos en que no tiene sentido llamar contains?, simplemente regresa false; Esto es lo que sucede en (contains? :foo 1) y también (contains? '(100 101 102) 101) . Actualización: en Clojure ≥ 1.5 contains?tiros cuando se entrega un objeto de un tipo que no admite la prueba de "membresía clave" prevista.

La forma correcta de hacer lo que intenta hacer es la siguiente:

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

Al buscar uno de los muchos elementos, puede usar un conjunto más grande; cuando busque false/ nil, puede usar false?/ nil?- porque (#{x} x)devuelve x, por lo tanto (#{nil} nil)es nil; al buscar uno de los múltiples elementos, algunos de los cuales pueden ser falseo nil, puede usar

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

(Tenga en cuenta que los elementos se pueden pasar a zipmapcualquier tipo de colección).

Michał Marczyk
fuente
Gracias Michal, ¡eres una fuente de sabiduría Clojure como siempre! Parece que voy a escribir mi propia función en este caso ... me sorprende un poco que todavía no haya una en el idioma central.
mikera
44
Como dijo Michal, ya hay una función en el núcleo que hace lo que deseas: algunas.
kotarak
2
Arriba, Michal comentó sobre (some #{101} '(100 101 102))decir que "la mayoría de las veces esto funciona". ¿No es justo decir que siempre funciona? Estoy usando Clojure 1.4 y la documentación usa este tipo de ejemplo. Funciona para mi y tiene sentido. ¿Hay algún tipo de caso especial donde no funciona?
David J.
77
@DavidJames: No funciona si está buscando la presencia de falseo nil- vea el siguiente párrafo. En una nota separada, en Clojure 1.5-RC1 contains?arroja una excepción cuando se le da una colección sin clave como argumento. Supongo que editaré esta respuesta cuando salga la versión final.
Michał Marczyk
1
¡Esto es estúpido! La principal distinción de una colección es la relación de membresía. Debería haber sido la función más importante para las colecciones. en.wikipedia.org/wiki/Set_(mathematics)#Membership
jgomo3
132

Aquí está mi utilidad estándar para el mismo propósito:

(defn in? 
  "true if coll contains elm"
  [coll elm]  
  (some #(= elm %) coll))
jg-faustus
fuente
36
Esta es la solución más simple y segura, ya que también maneja valores falsos como nily false. Ahora, ¿por qué esto no es parte de clojure / core?
Stian Soiland-Reyes
2
seq¿podría cambiar el nombre a coll, para evitar confusiones con la función seq?
nha
3
@nha Podrías hacer eso, sí. Aquí no importa: como no estamos usando la función seqdentro del cuerpo, no hay conflicto con el parámetro del mismo nombre. Pero siéntase libre de editar la respuesta si cree que el cambio de nombre facilitaría la comprensión.
jg-faustus
1
Vale la pena señalar que esto puede ser 3-4 veces más lento que (boolean (some #{elm} coll))si no tiene que preocuparse nilo false.
neverfox
2
@AviFlax Estaba pensando en clojure.org/guides/threading_macros , donde dice "Por convención, las funciones centrales que operan en secuencias esperan que la secuencia sea su último argumento. En consecuencia, las tuberías que contienen mapa, filtro, eliminar, reducir, etc. por lo general, llama a la macro >> Pero supongo que la convención trata más sobre funciones que operan en secuencias y secuencias de retorno.
John Wiseman
18

Siempre puede llamar a métodos java con la sintaxis .methodName.

(.contains [100 101 102] 101) => true
Yury Litvinov
fuente
55
En mi humilde opinión, esta es la mejor respuesta. Qué lástima clojure contiene? es tan confusamente llamado
mikkom
1
El venerable maestro Qc Na caminaba con su alumno, Anton. Cuando Anton le dijo que tenía problemas con los principiantes contains?, Qc Na lo golpeó con un Bô y dijo: "¡Estúpido estudiante! Debes darte cuenta de que no hay cuchara. ¡ Todo es solo Java debajo! Usa la notación de puntos". En ese momento, Anton se iluminó.
David Tonhofer
17

Sé que llego un poco tarde, pero ¿qué pasa con:

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

Por fin en clojure 1.4 salidas true :)

Giuliani Deon
fuente
3
(set '(101 102 103))es el mismo que %{101 102 103}. Entonces su respuesta se puede escribir como (contains? #{101 102 103} 102).
David J.
44
Esto tiene la desventaja de requerir la conversión de la lista original '(101 102 103)a un conjunto.
David J.
12
(not= -1 (.indexOf '(101 102 103) 102))

Funciona, pero a continuación es mejor:

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

Por lo que vale, esta es mi implementación simple de una función contiene para listas:

(defn list-contains? [coll value]
  (let [s (seq coll)]
    (if s
      (if (= (first s) value) true (recur (rest s) value))
      false)))
mikera
fuente
¿Podemos pedir la parte del predicado como argumento? Para obtener algo como:(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 tiene un vector o una lista y desea verificar si un valor está contenido en él, encontrará que contains?no funciona. Michał ya ha explicado por qué .

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

Hay cuatro cosas que puedes probar en este caso:

  1. Considere si realmente necesita un vector o una lista. Si utiliza un conjunto en su lugar , contains?funcionará.

    (contains? #{:a :b :c} :b) ; = true
  2. Usesome , envolviendo el objetivo en un conjunto, de la siguiente manera:

    (some #{:b} [:a :b :c]) ; = :b, which is truthy
  3. El acceso directo de establecer como función no funcionará si está buscando un valor falso ( falseo nil).

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

    En estos casos, debe usar la función de predicado incorporada para ese valor, false?o nil?:

    (some false? [true false true]) ; = true
  4. Si necesita hacer mucho este tipo de búsqueda, escriba una función para ello :

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

Además, vea la respuesta de Michał para ver formas de verificar si alguno de los múltiples objetivos están contenidos en una secuencia.

Rory O'Kane
fuente
5

Aquí hay una función rápida de mis utilidades estándar que uso para este propósito:

(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))))
GRAMO__
fuente
Sí, el tuyo tiene la ventaja de que se detendrá tan pronto como encuentre una coincidencia en lugar de continuar mapeando toda la secuencia.
G__
5

Aquí está la solución clásica de 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
fuente
44
De acuerdo, la razón por la que es una solución pobre en Clojure es que recurre a la pila en un procesador. Una mejor solución de Clojure es <pre> (defn member? [Elt col] (some # (= elt%) col)) </pre> Esto se debe a que somees potencialmente paralelo en los núcleos disponibles.
Simon Brooke
4

Me basé en la versión jg-faustus de "list-contiene?". Ahora toma cualquier número de argumentos.

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

Es tan simple como usar un conjunto: similar a los mapas, simplemente puede colocarlo en la posición de la función. Evalúa el valor si está en el conjunto (que es verdadero) o nil(que es falsey):

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

Si está comparando con un vector / lista de tamaño razonable que no tendrá hasta el tiempo de ejecución, también puede usar la setfunción:

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

La forma recomendada es utilizarlo somecon un conjunto; consulte la documentación para clojure.core/some.

A continuación, puede usar somedentro de un predicado verdadero verdadero / falso, por ejemplo

(defn in? [coll x] (if (some #{x} coll) true false))
KingCode
fuente
¿Por qué el if truey false? someya devuelve valores true-ish y false-ish.
subsub
¿qué pasa con (algunos # {nil} [nil])? Devuelve nil que se convertirá en 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))))))
David
fuente
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 )))

ejemplo de uso (¿cuál? [1 2 3] 3) o (cuál? # {1 2 3} 4 5 3)

Miguel
fuente
¿Todavía no se proporciona una función de núcleo de idioma para ello?
matanster
1

Como Clojure está construido en Java, puede llamar fácilmente al .indexOf función Java. Esta función devuelve el índice de cualquier elemento en una colección, y si no puede encontrar este elemento, devuelve -1.

Haciendo uso de esto, simplemente podríamos decir:

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

El problema con la solución 'recomendada' es que se rompe cuando el valor que busca es 'nulo'. Prefiero esta solución:

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

Hay funciones convenientes para este propósito en la biblioteca Tupelo . En particular, las funciones contains-elem?, contains-key?y contains-val?son muy útiles. La documentación completa está presente en los documentos de la API .

contains-elem?es el más genérico y está destinado a vectores o cualquier otra 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))))

Aquí vemos que para un rango entero o un vector mixto, contains-elem?funciona como se espera para los elementos existentes y no existentes en la colección. Para los mapas, también podemos buscar cualquier par clave-valor (expresado como un vector 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] ))))

También es sencillo buscar un conjunto:

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

Para mapas y conjuntos, es más simple (y más eficiente) usarlo contains-key?para encontrar una entrada de mapa o un elemento de conjunto:

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

Y, para los mapas, también puede buscar valores 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)))

Como se ve en la prueba, cada una de estas funciones funciona correctamente cuando se buscan nilvalores.

Alan Thompson
fuente
0

Otra opción:

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

Use java.util.Collection # contiene ():

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