Uji apakah daftar berisi nilai tertentu di Clojure

163

Apa cara terbaik untuk menguji apakah daftar berisi nilai yang diberikan di Clojure?

Secara khusus, perilaku contains?saat ini membingungkan saya:

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

Saya jelas bisa menulis fungsi sederhana untuk menelusuri daftar dan menguji kesetaraan, tetapi pasti ada cara standar untuk melakukan ini?

mikera
sumber
7
Aneh memang, mengandung? harus menjadi nama fungsi yang paling menyesatkan di Clojure :) Di sini berharap bahwa Clojure 1.3 akan melihatnya diubah namanya menjadi berisi-kunci? atau serupa.
jg-faustus
4
Saya pikir ini berbicara beberapa kali sekarang. mengandung? tidak akan berubah Lihat di sini: groups.google.com/group/clojure/msg/f2585c149cd0465d dan groups.google.com/group/clojure/msg/985478420223ecdf
kotarak
1
@kotarak terima kasih atas tautannya! Saya sebenarnya setuju dengan Rich di sini dalam hal penggunaan konten? nama meskipun saya pikir itu harus diubah untuk melempar kesalahan ketika diterapkan pada daftar atau urutan
mikera

Jawaban:

204

Ah, contains?... seharusnya salah satu dari lima FAQ teratas: Clojure.

Itu tidak memeriksa apakah koleksi berisi nilai; itu memeriksa apakah suatu item dapat diambil dengan getatau, dengan kata lain, apakah koleksi berisi kunci. Ini masuk akal untuk set (yang dapat dianggap sebagai membuat tidak ada perbedaan antara kunci dan nilai-nilai), peta (jadi (contains? {:foo 1} :foo)adalah true) dan vektor (tapi catatan itu (contains? [:foo :bar] 0)adalah true, karena kunci di sini adalah indeks dan vektor yang bersangkutan tidak "mengandung" yang indeks 0!).

Untuk menambah kebingungan, dalam kasus di mana tidak masuk akal untuk menelepon contains?, itu hanya kembali false; inilah yang terjadi di (contains? :foo 1) dan juga (contains? '(100 101 102) 101) . Pembaruan: Di Clojure ≥ 1,5 contains?lemparan ketika menyerahkan objek jenis yang tidak mendukung tes "keanggotaan kunci" yang dimaksud.

Cara yang benar untuk melakukan apa yang Anda coba lakukan adalah sebagai berikut:

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

Saat mencari salah satu dari banyak item, Anda dapat menggunakan set yang lebih besar; ketika mencari false/ nil, Anda dapat menggunakan false?/ nil?- karena (#{x} x)mengembalikan x, dengan demikian (#{nil} nil)adalah nil; saat mencari salah satu dari beberapa item yang beberapa di antaranya mungkin falseatau nil, Anda dapat menggunakan

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

(Perhatikan bahwa barang dapat dikirimkan ke zipmapdalam jenis koleksi apa pun.)

Michał Marczyk
sumber
Terima kasih Michal - Anda adalah font kearifan Clojure seperti biasa! Sepertinya saya akan menulis fungsi saya sendiri dalam kasus ini ... sedikit mengejutkan saya bahwa belum ada satu pun dalam bahasa inti.
mikera
4
Seperti yang dikatakan Michal - sudah ada fungsi dalam inti yang melakukan apa yang Anda inginkan: beberapa.
kotarak
2
Di atas, Michal berkomentar tentang (some #{101} '(100 101 102))mengatakan bahwa "sebagian besar waktu ini berhasil". Bukankah adil mengatakan bahwa itu selalu berhasil? Saya menggunakan Clojure 1.4 dan dokumentasi menggunakan contoh semacam ini. Ini bekerja untuk saya dan masuk akal. Apakah ada kasus khusus yang tidak berfungsi?
David J.
7
@ DavidJames: Tidak berfungsi jika Anda memeriksa keberadaan falseatau nil- lihat paragraf berikut. Pada catatan terpisah, di Clojure 1.5-RC1 contains?melempar pengecualian ketika diberi koleksi non-kunci sebagai argumen. Saya kira saya akan mengedit jawaban ini ketika rilis terakhir keluar.
Michał Marczyk
1
Ini bodoh! Perbedaan utama dari koleksi adalah hubungan keanggotaan. Seharusnya fungsi yang paling penting untuk koleksi. en.wikipedia.org/wiki/Set_(mathematics)#Membership
jgomo3
132

Inilah utilisasi standar saya untuk tujuan yang sama:

(defn in? 
  "true if coll contains elm"
  [coll elm]  
  (some #(= elm %) coll))
jg-faustus
sumber
36
Ini adalah solusi paling sederhana dan paling aman, karena ia juga menangani nilai-nilai palsu seperti nildan false. Sekarang mengapa ini bukan bagian dari clojure / core?
Stian Soiland-Reyes
2
seqmungkin bisa diubah namanya menjadi coll, untuk menghindari kebingungan dengan fungsi seq?
nha
3
@nha Anda bisa melakukan itu, ya. Tidak masalah di sini: Karena kita tidak menggunakan fungsi seqdi dalam tubuh, tidak ada konflik dengan parameter dengan nama yang sama. Tapi jangan ragu untuk mengedit jawabannya jika Anda berpikir penggantian nama akan membuatnya lebih mudah untuk dipahami.
jg-faustus
1
Perlu dicatat bahwa ini bisa 3-4x lebih lambat daripada (boolean (some #{elm} coll))jika Anda tidak perlu khawatir nilatau false.
neverfox
2
@AviFlax Saya berpikir tentang clojure.org/guides/threading_macros , di mana ia mengatakan "Secara konvensional, fungsi inti yang beroperasi pada urutan mengharapkan urutan sebagai argumen terakhir mereka. Dengan demikian, pipa yang berisi peta, filter, menghapus, mengurangi, ke, dll biasanya memanggil untuk - >> makro. " Tapi saya kira konvensi lebih lanjut tentang fungsi yang beroperasi pada urutan dan mengembalikan urutan.
John Wiseman
18

Anda selalu dapat memanggil metode java dengan sintaks .methodName.

(.contains [100 101 102] 101) => true
Yury Litvinov
sumber
5
IMHO ini adalah jawaban terbaik. Terlalu buruk clojure berisi? dinamai demikian membingungkan.
mikkom
1
Master terhormat Qc Na sedang berjalan bersama muridnya, Anton. Ketika Anton memberitahunya tentang memiliki masalah dengan pemula contains?, Qc Na memukulnya dengan Bô dan berkata: "Murid bodoh! Anda harus menyadari tidak ada sendok. Itu semua hanya Java di bawahnya! Gunakan notasi titik.". Pada saat itu, Anton menjadi tercerahkan.
David Tonhofer
17

Saya tahu bahwa saya sedikit terlambat, tetapi bagaimana dengan:

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

Akhirnya di clojure 1.4 output benar :)

Giuliani Deon
sumber
3
(set '(101 102 103))sama dengan %{101 102 103}. Jadi jawaban Anda dapat ditulis sebagai (contains? #{101 102 103} 102).
David J.
4
Ini memiliki kelemahan karena mengharuskan konversi daftar asli '(101 102 103)ke set.
David J.
12
(not= -1 (.indexOf '(101 102 103) 102))

Bekerja, tetapi di bawah ini lebih baik:

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

Untuk apa nilainya, ini adalah implementasi sederhana saya dari fungsi berisi daftar:

(defn list-contains? [coll value]
  (let [s (seq coll)]
    (if s
      (if (= (first s) value) true (recur (rest s) value))
      false)))
mikera
sumber
Bisakah kita meminta bagian predikat sebagai argumen? Untuk mendapatkan sesuatu seperti:(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

Jika Anda memiliki vektor atau daftar dan ingin memeriksa apakah suatu nilai terkandung di dalamnya, Anda akan menemukan bahwa contains?itu tidak berfungsi. Michał telah menjelaskan alasannya .

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

Ada empat hal yang dapat Anda coba dalam hal ini:

  1. Pertimbangkan apakah Anda benar-benar membutuhkan vektor atau daftar. Jika Anda menggunakan set sebagai gantinya , contains?akan berfungsi.

    (contains? #{:a :b :c} :b) ; = true
  2. Gunakansome , balutkan target dalam satu set, sebagai berikut:

    (some #{:b} [:a :b :c]) ; = :b, which is truthy
  3. Pintasan set-as-function tidak akan berfungsi jika Anda mencari nilai palsu ( falseatau nil).

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

    Dalam kasus ini, Anda harus menggunakan fungsi predikat bawaan untuk nilai itu, false?atau nil?:

    (some false? [true false true]) ; = true
  4. Jika Anda harus sering melakukan pencarian seperti ini, tuliskan fungsinya :

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

Juga, lihat jawaban Michał untuk cara memeriksa apakah ada dari beberapa target yang terkandung dalam suatu urutan.

Rory O'Kane
sumber
5

Berikut adalah fungsi cepat dari utilitas standar saya yang saya gunakan untuk tujuan ini:

(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__
sumber
Ya, milik Anda memiliki keuntungan bahwa itu akan berhenti segera setelah menemukan kecocokan daripada terus memetakan seluruh urutan.
G__
5

Inilah solusi Lisp klasik:

(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
sumber
4
OK, alasan yang merupakan solusi yang buruk di Clojure adalah karena ini mengulang tumpukan pada satu prosesor. Solusi Clojure yang lebih baik adalah <pre> (defn member? [Elt col] (some # (= elt%) col)) </pre> Ini karena someberpotensi paralel di seluruh core yang tersedia.
Simon Brooke
4

Saya telah membangun versi jg-faustus dari "daftar-berisi?". Sekarang dibutuhkan sejumlah argumen.

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

Ini sesederhana menggunakan set - mirip dengan peta, Anda bisa menjatuhkannya di posisi fungsi. Ini mengevaluasi nilai jika di set (yang benar) atau nil(yang palsu):

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

Jika Anda mengecek vektor / daftar berukuran cukup yang tidak akan Anda miliki sampai runtime, Anda juga dapat menggunakan set fungsi:

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

Cara yang disarankan adalah menggunakan somedokumentasi set - see untukclojure.core/some .

Anda kemudian dapat menggunakan somedalam predikat benar / salah nyata, mis

(defn in? [coll x] (if (some #{x} coll) true false))
KingCode
sumber
mengapa if truedan false? somesudah mengembalikan nilai true-ish dan false-ish.
subsub
bagaimana dengan (some # {nil} [nil])? Ini akan mengembalikan nil yang akan dikonversi menjadi false.
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
sumber
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 )))

contoh penggunaan (yang? [1 2 3] 3) atau (yang? # {1 2 3} 4 5 3)

Michael
sumber
masih tidak ada fungsi inti yang disediakan bahasa untuk itu?
matanster
1

Karena Clojure dibangun di Jawa, Anda dapat dengan mudah memanggilnya .indexOf fungsi Java. Fungsi ini mengembalikan indeks elemen apa pun dalam koleksi, dan jika tidak dapat menemukan elemen ini, mengembalikan -1.

Memanfaatkan ini kita bisa mengatakan:

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

Masalah dengan solusi 'disarankan' adalah rusak ketika nilai yang Anda cari adalah 'nihil'. Saya lebih suka solusi ini:

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

Ada fungsi yang nyaman untuk tujuan ini di perpustakaan Tupelo . Secara khusus, fungsi contains-elem?, contains-key?dan contains-val?sangat berguna. Dokumentasi lengkap ada di dokumen API .

contains-elem?adalah yang paling umum dan ditujukan untuk vektor atau clojure lainnya 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))))

Di sini kita melihat bahwa untuk rentang bilangan bulat atau vektor campuran, contains-elem?berfungsi seperti yang diharapkan untuk elemen yang ada dan tidak ada dalam koleksi. Untuk peta, kami juga dapat mencari pasangan nilai kunci apa saja (dinyatakan sebagai vektor 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] ))))

Juga mudah untuk mencari satu 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)))))

Untuk peta & set, lebih mudah (& lebih efisien) untuk digunakan contains-key?untuk menemukan entri peta atau elemen 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)))

Dan, untuk peta, Anda juga dapat mencari nilai dengan 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)))

Seperti yang terlihat dalam tes, masing-masing fungsi ini bekerja dengan benar ketika mencari nilnilai.

Alan Thompson
sumber
0

Pilihan lain:

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

Gunakan java.util.Collection # berisi ():

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