Teste se uma lista contém um valor específico no Clojure

163

Qual é a melhor maneira de testar se uma lista contém um determinado valor no Clojure?

Em particular, o comportamento de contains?está atualmente me confundindo:

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

Obviamente, eu poderia escrever uma função simples para percorrer a lista e testar a igualdade, mas certamente deve haver uma maneira padrão de fazer isso?

Mikera
fonte
7
Estranho, de fato, contém? deve ser a função mais enganosamente nomeada do Clojure :) Esperamos que o Clojure 1.3 o veja renomeado para contém-chave? ou similar.
Jg-faustus
4
Eu acho que isso já foi falado várias vezes agora. contém? não mudará. Consulte aqui: groups.google.com/group/clojure/msg/f2585c149cd0465d and groups.google.com/group/clojure/msg/985478420223ecdf
kotarak
1
@kotarak obrigado pelo link! Na verdade, eu concordo com Rich aqui em termos de uso do contém? nome que eu acho que deveria ser alterado para
gerar

Respostas:

204

Ah, contains?... supostamente uma das cinco principais perguntas frequentes sobre: ​​Clojure.

Ele não verifica se uma coleção contém um valor; verifica se um item pode ser recuperado com getou, em outras palavras, se uma coleção contém uma chave. Isso faz sentido para conjuntos (que podem ser considerados como não fazendo distinção entre chaves e valores), mapas (como (contains? {:foo 1} :foo)é true) e vetores (mas note que (contains? [:foo :bar] 0)é trueporque as chaves aqui são índices e o vetor em questão "contém" o índice 0!).

Para aumentar a confusão, nos casos em que não faz sentido ligar contains?, basta retornar false; é isso que acontece (contains? :foo 1) e também (contains? '(100 101 102) 101) . Atualização: No Clojure, contains?lançamentos ≥ 1,5 quando entregue um objeto de um tipo que não suporta o teste de "associação de chave" pretendido.

A maneira correta de fazer o que você está tentando fazer é a seguinte:

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

Ao procurar um de vários itens, você pode usar um conjunto maior; Ao procurar por false/ nil, você pode usar false?/ nil?- porque (#{x} x)retorna x, assim (#{nil} nil)é nil; ao procurar um dos vários itens, alguns dos quais podem ser falseou nil, você pode usar

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

(Observe que os itens podem ser passados ​​para zipmapqualquer tipo de coleção.)

Michał Marczyk
fonte
Obrigado Michal - você é uma fonte de sabedoria de Clojure, como sempre! Parece que vou escrever minha própria função neste caso ... me surpreende um pouco que ainda não exista uma na linguagem principal.
Mikera
4
Como Michal disse - já existe uma função no núcleo que faz o que você deseja: algumas.
Kotarak
2
Acima, Michal comentou sobre (some #{101} '(100 101 102))dizer que "na maioria das vezes isso funciona". Não é justo dizer que sempre funciona? Estou usando o Clojure 1.4 e a documentação usa esse tipo de exemplo. Funciona para mim e faz sentido. Existe algum tipo de caso especial em que não funciona?
David J.
7
@ DavidJames: Não funciona se você estiver verificando a presença de falseou nil- veja o parágrafo a seguir. Em uma observação separada, no Clojure 1.5-RC1 contains?lança uma exceção quando recebe uma coleção sem chave como argumento. Suponho que editarei esta resposta quando o lançamento final for lançado.
Michał Marczyk
1
Isso é estúpido! A principal distinção de uma coleção é a relação de associação. Deveria ter sido a função mais importante para coleções. en.wikipedia.org/wiki/Set_(mathematics)#Membership
jgomo3
132

Aqui está o meu utilitário padrão para o mesmo objetivo:

(defn in? 
  "true if coll contains elm"
  [coll elm]  
  (some #(= elm %) coll))
jg-faustus
fonte
36
Esta é a solução mais simples e segura, pois também lida com valores falsos como nile false. Agora, por que isso não faz parte do clojure / core?
Stian Soiland-Reyes
2
seqpoderia ser renomeado para coll, para evitar confusão com a função seq?
nha 22/02
3
@ nha Você poderia fazer isso, sim. Não importa aqui: como não estamos usando a função seqdentro do corpo, não há conflito com o parâmetro com o mesmo nome. Mas fique à vontade para editar a resposta se você acha que a renomeação facilitaria o entendimento.
Jg-faustus
1
Vale a pena notar que isso pode ser 3-4x mais lento do que (boolean (some #{elm} coll))se você não precisar se preocupar com nilou false.
Neverfox 19/02
2
@AviFlax Eu estava pensando em clojure.org/guides/threading_macros , onde diz "Por convenção, as funções principais que operam em seqüências esperam a sequência como seu último argumento. Assim, os pipelines que contêm o mapa, filtram, removem, reduzem, inserem etc. geralmente chame a macro - >>. " Mas acho que a convenção é mais sobre funções que operam em seqüências e sequências de retorno.
John Wiseman
18

Você sempre pode chamar métodos java com a sintaxe .methodName.

(.contains [100 101 102] 101) => true
Yury Litvinov
fonte
5
IMHO esta é a melhor resposta. Clojure muito ruim contém? tem um nome tão confuso.
Mikkom 9/03/16
1
O venerável mestre Qc Na estava andando com seu aluno, Anton. Quando Anton lhe contou sobre ter algum problema de iniciante contains?, Qc Na bateu nele com um Bô e disse: "Aluno estúpido! Você precisa perceber que não há colher. É tudo apenas Java por baixo! Use a notação de ponto". Nesse momento, Anton ficou iluminado.
David Tonhofer 7/09/19
17

Sei que estou um pouco atrasado, mas e quanto a:

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

Finalmente, no clojure 1.4, as saídas são verdadeiras :)

Giuliani Deon
fonte
3
(set '(101 102 103))é o mesmo que %{101 102 103}. Portanto, sua resposta pode ser escrita como (contains? #{101 102 103} 102).
David J.
4
Isso tem a desvantagem de exigir a conversão da lista original '(101 102 103)em um conjunto.
David J.
12
(not= -1 (.indexOf '(101 102 103) 102))

Funciona, mas abaixo é melhor:

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

Pelo que vale a pena, esta é minha implementação simples de uma função contém para listas:

(defn list-contains? [coll value]
  (let [s (seq coll)]
    (if s
      (if (= (first s) value) true (recur (rest s) value))
      false)))
Mikera
fonte
Podemos pedir a parte do predicado como argumento? Para obter 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 27/02
6

Se você tem um vetor ou lista e deseja verificar se um valor está contido nele, verá que contains?isso não funciona. Michał já explicou o porquê .

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

Há quatro coisas que você pode tentar neste caso:

  1. Considere se você realmente precisa de um vetor ou lista. Se você usar um conjunto , contains?funcionará.

    (contains? #{:a :b :c} :b) ; = true
  2. Usesome , agrupando o destino em um conjunto, da seguinte maneira:

    (some #{:b} [:a :b :c]) ; = :b, which is truthy
  3. O atalho de configuração como função não funcionará se você estiver procurando por um valor falso ( falseou nil).

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

    Nesses casos, você deve usar a função de predicado interno para esse valor false?ou nil?:

    (some false? [true false true]) ; = true
  4. Se você precisar fazer muito esse tipo de pesquisa, escreva uma função para ele :

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

Além disso, consulte a resposta de Michał para obter formas de verificar se algum dos vários destinos está contido em uma sequência.

Rory O'Kane
fonte
5

Aqui está uma função rápida dos meus utilitários padrão que eu uso para esse fim:

(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__
fonte
Sim, a sua tem a vantagem de que será interrompida assim que encontrar uma correspondência, em vez de continuar a mapear toda a sequência.
G__ 14/07/10
5

Aqui está a solução clássica do 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
fonte
4
OK, a razão pela qual a solução é ruim no Clojure é que ele recupera a pilha em um processador. Uma solução Clojure melhor é <pre> (defn member? [Elt col] (alguns # (= elt%) col)) </pre> Isso ocorre porque someé potencialmente paralelo entre os núcleos disponíveis.
Simon Brooke
4

Eu desenvolvi a versão jg-faustus de "list-contains?". Agora é preciso qualquer 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
fonte
2

É tão simples quanto usar um conjunto - semelhante aos mapas, você pode simplesmente deixá-lo na posição de função. Ele avalia o valor se no conjunto (que é verdade) ou nil(que é falsey):

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

Se você estiver verificando um vetor / lista de tamanho razoável que não terá até o tempo de execução, também poderá usar a setfunção:

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

A maneira recomendada é usar somecom um conjunto - consulte a documentação para clojure.core/some.

Você pode usar somedentro de um predicado verdadeiro / falso real, por exemplo

(defn in? [coll x] (if (some #{x} coll) true false))
KingCode
fonte
por que o if truee false? somejá retorna valores true-ish e false-ish.
subsub
e quanto a (alguns # {nada} [nada])? Ele retornaria nulo que será convertido em 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
fonte
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 )))

exemplo de uso (qual? ​​[1 2 3] 3) ou (qual? ​​# {1 2 3} 4 5 3)

Michael
fonte
ainda nenhuma função fornecida pelo núcleo da linguagem?
matanster
1

Como o Clojure é construído em Java, você também pode chamar facilmente a .indexOffunção Java. Essa função retorna o índice de qualquer elemento em uma coleção e, se não conseguir encontrar esse elemento, retorna -1.

Fazendo uso disso, poderíamos simplesmente dizer:

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

O problema com a solução 'recomendada' é que ela quebra quando o valor que você procura é 'nulo'. Eu prefiro esta solução:

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

Existem funções convenientes para esse fim na biblioteca Tupelo . Em particular, as funções contains-elem?, contains-key?e contains-val?são muito úteis. A documentação completa está presente nos documentos da API .

contains-elem?é o mais genérico e destina-se a vetores ou qualquer outro 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))))

Aqui vemos que, para um intervalo inteiro ou um vetor misto, contains-elem?funciona como esperado para elementos existentes e inexistentes na coleção. Para mapas, também podemos procurar qualquer par de valores-chave (expresso como um vetor 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] ))))

Também é simples pesquisar um 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 e conjuntos, é mais simples (e mais eficiente) usar contains-key?para encontrar uma entrada de mapa ou um 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)))

E, para mapas, você também pode procurar valores com 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 visto no teste, cada uma dessas funções funciona corretamente ao pesquisar nilvalores.

Alan Thompson
fonte
0

Outra opção:

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

Use java.util.Collection # contains ():

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