Apa saja kesalahan umum yang dibuat oleh pengembang Clojure, dan bagaimana kita bisa menghindarinya?
Sebagai contoh; pendatang baru di Clojure berpikir bahwa contains?
fungsinya sama seperti java.util.Collection#contains
. Namun, contains?
hanya akan berfungsi serupa saat digunakan dengan koleksi yang diindeks seperti peta dan set dan Anda mencari kunci yang diberikan:
(contains? {:a 1 :b 2} :b)
;=> true
(contains? {:a 1 :b 2} 2)
;=> false
(contains? #{:a 1 :b 2} :b)
;=> true
Ketika digunakan dengan koleksi yang diindeks secara numerik (vektor, array) contains?
hanya memeriksa bahwa elemen yang diberikan berada dalam kisaran indeks yang valid (berbasis nol):
(contains? [1 2 3 4] 4)
;=> false
(contains? [1 2 3 4] 0)
;=> true
Jika diberi daftar, contains?
tidak akan pernah mengembalikan nilai true.
some
fungsi Clojure atau, lebih baik lagi, gunakan sajacontains
. Implementasi koleksi Clojurejava.util.Collection
.(.contains [1 2 3] 2) => true
Jawaban:
Oktal Literal
Pada satu titik saya sedang membaca dalam matriks yang menggunakan nol di depan untuk mempertahankan baris dan kolom yang tepat. Secara matematis ini benar, karena nol di depan jelas tidak mengubah nilai yang mendasarinya. Upaya untuk menentukan var dengan matriks ini, bagaimanapun, akan gagal secara misterius dengan:
yang benar-benar membuatku bingung. Alasannya adalah bahwa Clojure memperlakukan nilai integer literal dengan nol di depannya sebagai oktal, dan tidak ada angka 08 dalam oktal.
Saya juga harus menyebutkan bahwa Clojure mendukung nilai heksadesimal Java tradisional melalui awalan 0x . Anda juga dapat menggunakan basis apa pun antara 2 dan 36 dengan menggunakan notasi "basis + r + nilai", seperti 2r101010 atau 36r16 yang merupakan 42 basis sepuluh.
Mencoba mengembalikan literal dalam fungsi literal anonim
Ini bekerja:
jadi saya yakin ini juga akan berhasil:
tapi gagal dengan:
karena makro pembaca # () diperluas ke
dengan literal peta yang dibungkus dengan tanda kurung. Karena ini adalah elemen pertama, ini diperlakukan sebagai fungsi (yang sebenarnya adalah peta literal), tetapi tidak ada argumen yang diperlukan (seperti kunci) yang disediakan. Singkatnya, literal fungsi anonim tidak meluas ke
sehingga Anda tidak dapat memiliki nilai literal ([],: a, 4,%) sebagai badan fungsi anonim.
Dua solusi telah diberikan di komentar. Brian Carper menyarankan menggunakan konstruktor implementasi urutan (array-map, hash-set, vektor) seperti:
sementara Dan menunjukkan bahwa Anda dapat menggunakan fungsi identitas untuk membuka tanda kurung luar:
Saran Brian benar-benar membawa saya ke kesalahan saya berikutnya ...
Berpikir bahwa hash-map atau array-map menentukan implementasi peta konkret yang tidak berubah
Pertimbangkan hal berikut:
Meskipun Anda secara umum tidak perlu khawatir tentang implementasi konkret peta Clojure, Anda harus tahu bahwa fungsi yang mengembangkan peta - seperti assoc atau conj - dapat menggunakan PersistentArrayMap dan mengembalikan PersistentHashMap , yang bekerja lebih cepat untuk peta yang lebih besar.
Menggunakan fungsi sebagai titik rekursi daripada sebuah loop untuk menyediakan binding awal
Ketika saya memulai, saya menulis banyak fungsi seperti ini:
Sebenarnya loop akan lebih ringkas dan idiomatik untuk fungsi khusus ini:
Perhatikan bahwa saya mengganti argumen kosong, badan fungsi "konstruktor default" (p3 775147 600851475143 3) dengan loop + pengikatan awal. The terulang sekarang rebinds binding lingkaran (bukan parameter fn) dan melompat kembali ke titik rekursi (lingkaran, bukan fn).
Mengacu pada vars "phantom"
Saya berbicara tentang jenis var yang mungkin Anda tentukan menggunakan REPL - selama pemrograman eksplorasi Anda - kemudian tanpa disadari merujuk ke sumber Anda. Semuanya berfungsi dengan baik sampai Anda memuat ulang namespace (mungkin dengan menutup editor Anda) dan kemudian menemukan banyak simbol tak terikat yang direferensikan di seluruh kode Anda. Ini juga sering terjadi saat Anda melakukan refactoring, memindahkan var dari satu namespace ke yang lain.
Memperlakukan pemahaman daftar for seperti perintah for loop
Pada dasarnya Anda membuat daftar malas berdasarkan daftar yang ada daripada hanya melakukan loop terkontrol. Dosis Clojure sebenarnya lebih analog untuk setiap konstruksi perulangan yang penting.
Salah satu contoh perbedaannya adalah kemampuan untuk memfilter elemen mana yang diiterasi menggunakan predikat arbitrer:
Cara lain mereka berbeda adalah mereka dapat beroperasi pada urutan malas yang tak terbatas:
Mereka juga dapat menangani lebih dari satu ekspresi yang mengikat, mengulang ekspresi paling kanan terlebih dahulu dan bekerja ke kiri:
Ada juga yang tidak istirahat atau terus keluar sebelum waktunya.
Terlalu sering menggunakan struct
Saya berasal dari latar belakang OOPish jadi ketika saya memulai Clojure, otak saya masih berpikir dalam kerangka objek. Saya mendapati diri saya memodelkan semuanya sebagai sebuah struct karena pengelompokan "anggota", betapapun longgar, membuat saya merasa nyaman. Pada kenyataannya, sebagian besar struct harus dianggap sebagai pengoptimalan; Clojure akan membagikan kunci dan beberapa informasi pencarian untuk menghemat memori. Anda dapat lebih mengoptimalkannya dengan menentukan pengakses untuk mempercepat proses pencarian kunci.
Secara keseluruhan Anda tidak mendapatkan apa pun dari penggunaan struct di atas peta kecuali untuk kinerja, jadi kerumitan tambahan mungkin tidak sepadan.
Menggunakan konstruktor BigDecimal tidak berbobot
Saya membutuhkan banyak BigDecimals dan menulis kode jelek seperti ini:
padahal sebenarnya Clojure mendukung literal BigDecimal dengan menambahkan M ke angka:
Menggunakan versi manis akan mengurangi banyak penggembungan. Dalam komentar, twils menyebutkan bahwa Anda juga dapat menggunakan fungsi bigdec dan bigint agar lebih eksplisit, namun tetap ringkas.
Menggunakan konversi penamaan paket Java untuk namespace
Ini sebenarnya bukan kesalahan, melainkan sesuatu yang bertentangan dengan struktur idiomatik dan penamaan proyek Clojure yang khas. Proyek Clojure substansial pertama saya memiliki deklarasi namespace - dan struktur folder yang sesuai - seperti ini:
yang membengkak referensi fungsi saya yang memenuhi syarat:
Untuk memperumit banyak hal, saya menggunakan struktur direktori Maven standar :
yang lebih kompleks daripada struktur Clojure "standar" dari:
yang merupakan default dari proyek Leiningen dan Clojure itu sendiri.
Peta menggunakan persamaan Java () daripada Clojure = untuk pencocokan kunci
Awalnya dilaporkan oleh chouser di IRC , penggunaan persamaan Java () ini mengarah ke beberapa hasil yang tidak intuitif:
Karena instance Integer dan Long 1 dicetak sama secara default, akan sulit untuk mendeteksi mengapa peta Anda tidak mengembalikan nilai apa pun. Ini terutama benar ketika Anda melewatkan kunci Anda melalui suatu fungsi yang, mungkin tanpa Anda ketahui, menghasilkan nilai long.
Perlu dicatat bahwa menggunakan persamaan Java () daripada Clojure = sangat penting untuk peta agar sesuai dengan antarmuka java.util.Map.
Saya menggunakan Programming Clojure oleh Stuart Halloway, Practical Clojure oleh Luke VanderHart, dan bantuan dari banyak hacker Clojure di IRC dan milis untuk membantu jawaban saya.
sumber
(#(hash-set %1 %2) :a 1)
atau dalam kasus ini(hash-set :a 1)
.do
:(#(do {%1 %2}) :a 1)
.hash-map
langsung (seperti di(hash-map :a 1)
atau(map hash-map keys vals)
) lebih mudah dibaca dan tidak menyiratkan bahwa sesuatu yang istimewa dan masih belum diimplementasikan dalam fungsi bernama sedang berlangsung (yang menurut saya#(...)
tidak disiratkan oleh penggunaan ). Faktanya, terlalu sering menggunakan fns anonim adalah hal yang harus dipikirkan sendiri. :-) OTOH, terkadang saya menggunakando
fungsi anonim yang sangat ringkas yang bebas efek samping ... Ini cenderung terlihat jelas bahwa fungsi tersebut hanya dalam satu pandangan. Masalah rasa, kurasa.Lupa memaksa evaluasi urutan malas
Urutan malas tidak dievaluasi kecuali Anda meminta mereka untuk dievaluasi. Anda mungkin mengharapkan ini untuk mencetak sesuatu, tetapi ternyata tidak.
Tidak
map
pernah dievaluasi, dibuang secara diam-diam, karena malas. Anda harus menggunakan salahdoseq
,dorun
,doall
dll untuk memaksa evaluasi urutan malas untuk efek samping.Menggunakan telanjang
map
di jenis REPL sepertinya berhasil, tetapi hanya berfungsi karena REPL memaksa evaluasi urutan malas itu sendiri. Ini dapat membuat bug lebih sulit untuk diketahui, karena kode Anda berfungsi di REPL dan tidak berfungsi dari file sumber atau di dalam suatu fungsi.sumber
(map ...)
dari dalam(binding ...)
dan bertanya-tanya mengapa nilai-nilai baru yang mengikat tidak berlaku.Saya seorang noob Clojure. Pengguna yang lebih mahir mungkin memiliki masalah yang lebih menarik.
mencoba mencetak urutan malas yang tak terbatas.
Saya tahu apa yang saya lakukan dengan urutan malas saya, tetapi untuk tujuan debugging saya memasukkan beberapa panggilan print / prn / pr, sementara lupa apa yang saya cetak. Lucu, kenapa PC saya mati semua?
mencoba memprogram Clojure secara imperatif.
Ada beberapa godaan untuk membuat banyak
ref
s atauatom
s dan menulis kode yang terus-menerus mengacaukan statusnya. Ini bisa dilakukan, tapi tidak cocok. Ini mungkin juga memiliki kinerja yang buruk, dan jarang mendapat manfaat dari banyak inti.mencoba memprogram Clojure 100% secara fungsional.
Sisi lain dari ini: Beberapa algoritme benar-benar menginginkan status yang bisa berubah. Secara religius, menghindari status yang dapat berubah dengan cara apa pun dapat mengakibatkan algoritme yang lambat atau canggung. Dibutuhkan penilaian dan sedikit pengalaman untuk membuat keputusan.
mencoba melakukan terlalu banyak hal di Java.
Karena sangat mudah menjangkau Java, terkadang tergoda untuk menggunakan Clojure sebagai pembungkus bahasa skrip di sekitar Java. Tentunya Anda harus melakukan hal ini dengan tepat saat menggunakan fungsionalitas pustaka Java, tetapi tidak ada gunanya (misalnya) memelihara struktur data di Java, atau menggunakan tipe data Java seperti koleksi yang padanannya bagus di Clojure.
sumber
Banyak hal sudah disebutkan. Saya hanya akan menambahkan satu lagi.
Clojure jika memperlakukan objek Java Boolean selalu benar meskipun nilainya salah. Jadi jika Anda memiliki fungsi java land yang mengembalikan nilai java Boolean, pastikan Anda tidak memeriksanya secara langsung,
(if java-bool "Yes" "No")
melainkan(if (boolean java-bool) "Yes" "No")
.Saya terbakar oleh ini dengan pustaka clojure.contrib.sql yang mengembalikan bidang boolean database sebagai objek java Boolean.
sumber
(if java.lang.Boolean/FALSE (println "foo"))
tidak mencetak foo.(if (java.lang.Boolean. "false") (println "foo"))
melakukannya, meskipun(if (boolean (java.lang.Boolean "false")) (println "foo"))
tidak ... Cukup membingungkan!nil
danfalse
salah, dan yang lainnya benar. A JavaBoolean
tidaknil
dan bukanfalse
(karena itu adalah objek), jadi perilakunya konsisten.Menjaga kepala Anda tetap dalam lingkaran.
Anda berisiko kehabisan memori jika melakukan loop pada elemen urutan malas yang berpotensi sangat besar atau tak terbatas sambil mempertahankan referensi ke elemen pertama.
Lupa tidak ada TCO.
Tail-call biasa menghabiskan ruang tumpukan, dan akan meluap jika Anda tidak berhati-hati. Clojure memiliki
'recur
dan'trampoline
menangani banyak kasus di mana tail-call yang dioptimalkan akan digunakan dalam bahasa lain, tetapi teknik ini harus diterapkan dengan sengaja.Urutan yang tidak terlalu malas.
Anda dapat membuat urutan malas dengan
'lazy-seq
atau'lazy-cons
(atau dengan membangun di atas API malas tingkat yang lebih tinggi), tetapi jika Anda menggabungkannya'vec
atau meneruskannya melalui beberapa fungsi lain yang menyadari urutan tersebut, maka urutan tersebut tidak akan lagi menjadi malas. Baik tumpukan dan heap dapat dilampaui oleh ini.Menempatkan hal-hal yang bisa berubah di referensi.
Secara teknis Anda dapat melakukannya, tetapi hanya referensi objek di ref itu sendiri yang diatur oleh STM - bukan objek yang dirujuk dan bidangnya (kecuali jika referensi tersebut tidak dapat diubah dan mengarah ke referensi lain). Jadi, jika memungkinkan, pilihlah objek yang tidak dapat diubah dalam referensi. Hal yang sama berlaku untuk atom.
sumber
digunakan
loop ... recur
untuk memproses urutan saat peta akan dilakukan.vs.
Fungsi peta (di cabang terbaru) menggunakan urutan potongan dan banyak pengoptimalan lainnya. Selain itu, karena fungsi ini sering dijalankan, Hotspot JIT biasanya sudah dioptimalkan dan siap digunakan tanpa "waktu pemanasan".
sumber
work
Fungsi Anda setara dengan(doseq [item data] (do-stuff item))
. (Selain fakta, putaran dalam pekerjaan itu tidak pernah berakhir.)map
dan / ataureduce
.Jenis koleksi memiliki perilaku berbeda untuk beberapa operasi:
Bekerja dengan string bisa membingungkan (saya masih belum mengerti). Secara khusus, string tidak sama dengan urutan karakter, meskipun fungsi urutan bekerja padanya:
Untuk mengeluarkan senar, Anda perlu melakukan:
sumber
terlalu banyak tanda kurung, terutama dengan panggilan metode java void di dalamnya yang menghasilkan NPE:
menghasilkan NPE dari tanda kurung luar karena tanda kurung bagian dalam bernilai nol.
menghasilkan lebih mudah untuk men-debug:
sumber