Kesalahan pemrograman umum yang harus dihindari pengembang Clojure [ditutup]

92

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.

fogus
sumber
4
FYI saja, bagi pengembang Clojure yang mencari java.util.Collection # berisi fungsionalitas tipe, lihat clojure.contrib.seq-utils / include? Dari dokumen: Penggunaan: (termasuk? Coll x). Mengembalikan nilai benar jika coll berisi sesuatu yang sama (dengan =) ke x, dalam waktu linier.
Robert Campbell
11
Anda sepertinya melewatkan fakta bahwa pertanyaan tersebut adalah Wiki Komunitas
3
Saya suka bagaimana pertanyaan Perl harus keluar dari langkah dengan yang lain :)
Ether
8
Untuk pengembang Clojure yang mencari konten, saya sarankan untuk tidak mengikuti saran rcampbell. seq-utils telah lama tidak digunakan lagi dan fungsi itu tidak pernah berguna untuk memulai. Anda bisa menggunakan somefungsi Clojure atau, lebih baik lagi, gunakan saja contains. Implementasi koleksi Clojure java.util.Collection. (.contains [1 2 3] 2) => true
Rayne

Jawaban:

70

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:

java.lang.NumberFormatException: Invalid number: 08

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:

user> (defn foo [key val]
    {key val})
#'user/foo
user> (foo :a 1)
{:a 1}

jadi saya yakin ini juga akan berhasil:

(#({%1 %2}) :a 1)

tapi gagal dengan:

java.lang.IllegalArgumentException: Wrong number of args passed to: PersistentArrayMap

karena makro pembaca # () diperluas ke

(fn [%1 %2] ({%1 %2}))  

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

(fn [%1 %2] {%1 %2})  ; notice the lack of parenthesis

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:

(#(array-map %1 %2) :a 1)

sementara Dan menunjukkan bahwa Anda dapat menggunakan fungsi identitas untuk membuka tanda kurung luar:

(#(identity {%1 %2}) :a 1)

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:

user> (class (hash-map))
clojure.lang.PersistentArrayMap
user> (class (hash-map :a 1))
clojure.lang.PersistentHashMap
user> (class (assoc (apply array-map (range 2000)) :a :1))
clojure.lang.PersistentHashMap

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:

; Project Euler #3
(defn p3 
  ([] (p3 775147 600851475143 3))
  ([i n times]
    (if (and (divides? i n) (fast-prime? i times)) i
      (recur (dec i) n times))))

Sebenarnya loop akan lebih ringkas dan idiomatik untuk fungsi khusus ini:

; Elapsed time: 387 msecs
(defn p3 [] {:post [(= % 6857)]}
  (loop [i 775147 n 600851475143 times 3]
    (if (and (divides? i n) (fast-prime? i times)) i
      (recur (dec i) n times))))

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:

user> (for [n '(1 2 3 4) :when (even? n)] n)
(2 4)

user> (for [n '(4 3 2 1) :while (even? n)] n)
(4)

Cara lain mereka berbeda adalah mereka dapat beroperasi pada urutan malas yang tak terbatas:

user> (take 5 (for [x (iterate inc 0) :when (> (* x x) 3)] (* 2 x)))
(4 6 8 10 12)

Mereka juga dapat menangani lebih dari satu ekspresi yang mengikat, mengulang ekspresi paling kanan terlebih dahulu dan bekerja ke kiri:

user> (for [x '(1 2 3) y '(\a \b \c)] (str x y))
("1a" "1b" "1c" "2a" "2b" "2c" "3a" "3b" "3c")

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:

(let [foo (BigDecimal. "1") bar (BigDecimal. "42.42") baz (BigDecimal. "24.24")]

padahal sebenarnya Clojure mendukung literal BigDecimal dengan menambahkan M ke angka:

(= (BigDecimal. "42.42") 42.42M) ; true

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:

(ns com.14clouds.myapp.repository)

yang membengkak referensi fungsi saya yang memenuhi syarat:

(com.14clouds.myapp.repository/load-by-name "foo")

Untuk memperumit banyak hal, saya menggunakan struktur direktori Maven standar :

|-- src/
|   |-- main/
|   |   |-- java/
|   |   |-- clojure/
|   |   |-- resources/
|   |-- test/
...

yang lebih kompleks daripada struktur Clojure "standar" dari:

|-- src/
|-- test/
|-- resources/

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:

user> (= (int 1) (long 1))
true
user> ({(int 1) :found} (int 1) :not-found)
:found
user> ({(int 1) :found} (long 1) :not-found)
:not-found

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.

rcampbell
sumber
1
Semua makro pembaca memiliki versi fungsi normal. Anda bisa melakukan (#(hash-set %1 %2) :a 1)atau dalam kasus ini (hash-set :a 1).
Brian Carper
2
Anda juga dapat 'menghapus' tanda kurung tambahan dengan identitas: (# (identitas {% 1% 2}): a 1)
1
Anda juga bisa menggunakan do: (#(do {%1 %2}) :a 1).
Michał Marczyk
@ Michał - Saya tidak menyukai solusi ini sebanyak yang sebelumnya karena do menyiratkan bahwa efek samping sedang terjadi, padahal sebenarnya hal ini tidak terjadi di sini.
Robert Campbell
@ rrc7cz: Sebenarnya, di sini tidak perlu menggunakan fungsi anonim sama sekali, karena menggunakan hash-maplangsung (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 menggunakan dofungsi anonim yang sangat ringkas yang bebas efek samping ... Ini cenderung terlihat jelas bahwa fungsi tersebut hanya dalam satu pandangan. Masalah rasa, kurasa.
Michał Marczyk
42

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.

user=> (defn foo [] (map println [:foo :bar]) nil)
#'user/foo
user=> (foo)
nil

Tidak mappernah dievaluasi, dibuang secara diam-diam, karena malas. Anda harus menggunakan salah doseq, dorun, doalldll untuk memaksa evaluasi urutan malas untuk efek samping.

user=> (defn foo [] (doseq [x [:foo :bar]] (println x)) nil)
#'user/foo
user=> (foo)
:foo
:bar
nil
user=> (defn foo [] (dorun (map println [:foo :bar])) nil)
#'user/foo
user=> (foo)
:foo
:bar
nil

Menggunakan telanjang mapdi 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.

user=> (map println [:foo :bar])
(:foo
:bar
nil nil)
Brian Carper
sumber
1
+1. Ini menggigit saya, tetapi dengan cara yang lebih berbahaya: Saya mengevaluasi (map ...)dari dalam (binding ...)dan bertanya-tanya mengapa nilai-nilai baru yang mengikat tidak berlaku.
Alex B
20

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 refs atau atoms 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.

Carl Smotricz
sumber
13

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.

Vagif Verdi
sumber
8
Perhatikan bahwa (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!
Michał Marczyk
Tampaknya berfungsi seperti yang diharapkan di Clojure 1.4.0: (assert (=: false (if Boolean / FALSE: true: false)))
Jakub Holý
Saya juga terbakar oleh yang satu ini baru-baru ini ketika melakukan (filter: mykey coll) di mana: nilai mykey di mana Booleans - berfungsi seperti yang diharapkan dengan koleksi yang dibuat Clojure, tetapi BUKAN dengan koleksi deserialisasi, ketika diserialisasi menggunakan serialisasi Java default - karena Boolean tersebut deserialisasi sebagai Boolean baru (), dan sayangnya (Boolean baru (true)! = java.lang.Boolean / TRUE)
Hendekagon
1
Ingat saja aturan dasar nilai Boolean di Clojure - nildan falsesalah, dan yang lainnya benar. A Java Booleantidak nildan bukan false(karena itu adalah objek), jadi perilakunya konsisten.
erikprice
13

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 'recurdan 'trampolinemenangani 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-seqatau 'lazy-cons(atau dengan membangun di atas API malas tingkat yang lebih tinggi), tetapi jika Anda menggabungkannya 'vecatau 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.

Chris Vest
sumber
4
cabang pengembangan yang akan datang sangat membantu mengurangi item pertama dengan menghapus referensi ke objek dalam suatu fungsi setelah mereka menjadi tidak dapat dijangkau secara lokal.
Arthur Ulfeldt
9

digunakan loop ... recuruntuk memproses urutan saat peta akan dilakukan.

(defn work [data]
    (do-stuff (first data))
    (recur (rest data)))

vs.

(map do-stuff data)

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".

Arthur Ulfeldt
sumber
1
Kedua versi ini sebenarnya tidak setara. workFungsi Anda setara dengan (doseq [item data] (do-stuff item)). (Selain fakta, putaran dalam pekerjaan itu tidak pernah berakhir.)
kotarak
ya, yang pertama mematahkan kemalasan dalam argumennya. urutan yang dihasilkan akan memiliki nilai yang sama meskipun tidak lagi menjadi urutan yang malas.
Arthur Ulfeldt
+1! Saya menulis banyak fungsi rekursif kecil hanya untuk menemukan hari lain bahwa semua ini dapat digeneralisasikan dengan menggunakan mapdan / atau reduce.
nperson325681
5

Jenis koleksi memiliki perilaku berbeda untuk beberapa operasi:

user=> (conj '(1 2 3) 4)    
(4 1 2 3)                 ;; new element at the front
user=> (conj [1 2 3] 4) 
[1 2 3 4]                 ;; new element at the back

user=> (into '(3 4) (list 5 6 7))
(7 6 5 3 4)
user=> (into [3 4] (list 5 6 7)) 
[3 4 5 6 7]

Bekerja dengan string bisa membingungkan (saya masih belum mengerti). Secara khusus, string tidak sama dengan urutan karakter, meskipun fungsi urutan bekerja padanya:

user=> (filter #(> (int %) 96) "abcdABCDefghEFGH")
(\a \b \c \d \e \f \g \h)

Untuk mengeluarkan senar, Anda perlu melakukan:

user=> (apply str (filter #(> (int %) 96) "abcdABCDefghEFGH"))
"abcdefgh"
Matt Fenwick
sumber
3

terlalu banyak tanda kurung, terutama dengan panggilan metode java void di dalamnya yang menghasilkan NPE:

public void foo() {}

((.foo))

menghasilkan NPE dari tanda kurung luar karena tanda kurung bagian dalam bernilai nol.

public int bar() { return 5; }

((.bar)) 

menghasilkan lebih mudah untuk men-debug:

java.lang.Integer cannot be cast to clojure.lang.IFn
  [Thrown class java.lang.ClassCastException]
miaubiz
sumber