Clojure geliştiricilerinin kaçınması gereken yaygın programlama hataları [kapalı]


92

Clojure geliştiricilerinin yaptığı bazı yaygın hatalar nelerdir ve bunlardan nasıl kaçınabiliriz?

Örneğin; Clojure'e yeni gelenler, contains?işlevin aynı şekilde çalıştığını düşünüyor java.util.Collection#contains. Ancak, contains?yalnızca haritalar ve setler gibi dizine alınmış koleksiyonlarla kullanıldığında ve belirli bir anahtarı aradığınızda benzer şekilde çalışır:

(contains? {:a 1 :b 2} :b)
;=> true
(contains? {:a 1 :b 2} 2)
;=> false
(contains? #{:a 1 :b 2} :b)
;=> true

Sayısal olarak dizinlenmiş koleksiyonlarla (vektörler, diziler) kullanıldığında, contains? yalnızca belirli öğenin geçerli dizin aralığı içinde (sıfır tabanlı) olup olmadığını kontrol eder:

(contains? [1 2 3 4] 4)
;=> false
(contains? [1 2 3 4] 0)
;=> true

Bir liste verilirse, contains?asla doğru dönmez.


4
Bilginize, java.util.Collection # tür işlevini arayan Clojure geliştiricileri için clojure.contrib.seq-utils / includes? Dokümanlardan: Kullanım: (? Coll x içerir). Coll, doğrusal zamanda x'e eşit (= ile) bir şey içeriyorsa doğru döndürür.
Robert Campbell

11
Sen bu sorular Topluluk Wiki olduğu gerçeğini kaçırmış görünüyor

3
Perl sorusunun diğerleriyle nasıl uyumsuz olması gerektiğine bayılıyorum :)
Ether

8
İçindekileri arayan Clojure geliştiricileri için rcampbell'in tavsiyelerine uymamanızı tavsiye ederim. seq-utils uzun zamandır kullanımdan kaldırıldı ve bu işlevin başlaması hiçbir zaman yararlı olmadı. Clojure'un someişlevini kullanabilir veya daha da iyisi, sadece containskendisini kullanabilirsiniz . Clojure koleksiyonları uygulanır java.util.Collection. (.contains [1 2 3] 2) => true
Rayne

Yanıtlar:


70

Sabit Sekizli

Bir noktada, doğru satırları ve sütunları korumak için baştaki sıfırları kullanan bir matriste okuyordum. Matematiksel olarak bu doğrudur, çünkü sıfırın başındaki değer açıkça altta yatan değeri değiştirmez. Bununla birlikte, bu matrisle bir değişken tanımlama girişimleri şunlarla gizemli bir şekilde başarısız olur:

java.lang.NumberFormatException: Invalid number: 08

bu beni tamamen şaşırttı. Bunun nedeni, Clojure'un baştaki sıfırlar ile tam sayı değerlerini sekizli olarak ele alması ve sekizlik tabanda 08 sayısının olmamasıdır.

Clojure'un 0x öneki aracılığıyla geleneksel Java onaltılık değerlerini desteklediğini de belirtmeliyim . Ayrıca "taban + r + değer" gösterimini kullanarak 2 ile 36 arasındaki herhangi bir tabanı da kullanabilirsiniz, örneğin 2r101010 veya 36r16 42 taban on olan.


Anonim bir işlev değişmez değerinde değişmez değerleri döndürmeye çalışma

Bu çalışıyor:

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

bu yüzden bunun da işe yarayacağına inandım:

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

ancak şununla başarısız olur:

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

çünkü # () okuyucu makrosu,

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

harita değişmez değeri parantez içine alınır. İlk öğe olduğu için, bir işlev olarak ele alınır (gerçek bir harita aslında budur), ancak gerekli bağımsız değişkenler (anahtar gibi) sağlanmaz. Özetle, anonim işlev değişmezi gelmez değil genişletmek

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

ve bu nedenle anonim işlevin gövdesi olarak herhangi bir gerçek değere ([],: a, 4,%) sahip olamazsınız.

Yorumlarda iki çözüm verildi. Brian Carper , aşağıdaki gibi dizi uygulama oluşturucularının (dizi haritası, karma küme, vektör) kullanılmasını önerir:

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

ederken Dan gösterileri kendinizin kullanabileceği kimlik dış parantez unwrap işlevi:

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

Brian'ın önerisi aslında beni bir sonraki hatama getiriyor ...


Karma haritanın veya dizi haritasının değişmeyen somut harita uygulamasını belirlediğini düşünmek

Aşağıdakileri göz önünde bulundur:

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

Genelde bir Clojure haritasının somut uygulaması hakkında endişelenmenize gerek kalmazken, assoc veya conj gibi bir harita oluşturan işlevlerin bir PersistentArrayMap alıp daha büyük haritalar için daha hızlı performans gösteren bir PersistentHashMap döndürebileceğini bilmelisiniz .


İlk bağlamaları sağlamak için bir döngü yerine özyineleme noktası olarak bir işlevi kullanma

Başladığımda, bunun gibi birçok işlev yazdım:

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

Aslında döngü , bu belirli işlev için daha kısa ve deyimsel olduğunda:

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

Boş bağımsız değişken olan "varsayılan yapıcı" işlev gövdesini (p3 775147 600851475143 3) bir döngü + ilk bağlamayla değiştirdiğime dikkat edin. Tekrarlanmasını şimdi (yerine fn parametrelerin) döngü bağlantıları rebinds ve (yerine fn arasında döngü) geri tekrarlama noktasına atlar.


"Hayali" değişkenlere başvurma

Keşif programlamanız sırasında REPL'i kullanarak tanımlayabileceğiniz var tipinden bahsediyorum ve sonra bilmeden kaynağınızda referans alın. Siz ad alanını yeniden yükleyene kadar (belki düzenleyicinizi kapatarak) ve daha sonra kodunuz boyunca referans verilen bir grup bağlantısız sembol keşfedene kadar her şey yolunda gider. Bu aynı zamanda, bir değişkeni bir ad alanından diğerine taşırken, yeniden düzenleme yaparken sık sık olur.


For list anlayışına for döngüsü zorunluluğu gibi davranmak

Esasen, sadece kontrollü bir döngü gerçekleştirmek yerine mevcut listelere dayalı bir tembel liste oluşturuyorsunuz. Clojure en doseq aslında zorunlu foreach döngü yapılara daha benzer.

Nasıl farklı olduklarına bir örnek, keyfi yüklemler kullanarak hangi öğeleri yinelediklerini filtreleme yeteneğidir:

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)

Farklı olmalarının bir başka yolu da sonsuz tembel diziler üzerinde çalışabilmeleridir:

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

Ayrıca, önce en sağdaki ifadeyi yineleyerek ve sola doğru ilerleyerek birden fazla ciltleme ifadesini de işleyebilirler:

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

Ayrıca ara yok veya erken çıkmaya devam et.


Yapıların aşırı kullanımı

OOPish bir geçmişe sahip olduğum için Clojure'e başladığımda beynim hala nesneler açısından düşünüyordu. Kendimi her şeyi bir yapı olarak modellerken buldum çünkü "üyelerin" gruplanması, ne kadar gevşek olursa olsun, beni rahat hissettiriyordu. Gerçekte, yapılar çoğunlukla bir optimizasyon olarak düşünülmelidir; Clojure, belleği korumak için anahtarları ve bazı arama bilgilerini paylaşır. Anahtar arama sürecini hızlandırmak için erişimciler tanımlayarak bunları daha da optimize edebilirsiniz .

Genel olarak , performans dışında bir harita üzerinde yapı kullanmaktan hiçbir şey kazanmazsınız , bu nedenle eklenen karmaşıklık buna değmeyebilir.


Şekersiz BigDecimal oluşturucuları kullanma

Çok fazla BigDecimals'a ihtiyacım vardı ve şöyle çirkin kod yazıyordum:

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

Clojure , sayıya M ekleyerek BigDecimal değişmez değerlerini desteklediğinde :

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

Şekerli versiyonu kullanmak şişkinliğin çoğunu ortadan kaldırır. Yorumlarda twils , bigdec ve bigint işlevlerini daha açık ve özlü kalabilmek için de kullanabileceğinizi belirtti .


Ad alanları için Java paketi adlandırma dönüşümlerini kullanma

Bu aslında kendi başına bir hata değil, daha ziyade tipik bir Clojure projesinin deyimsel yapısına ve isimlendirmesine aykırı bir şey. İlk önemli Clojure projemde aşağıdaki gibi ad alanı bildirimleri ve bunlara karşılık gelen klasör yapıları vardı:

(ns com.14clouds.myapp.repository)

tam nitelikli işlev referanslarımı şişiren:

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

İşleri daha da karmaşık hale getirmek için standart bir Maven dizin yapısı kullandım:

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

Bu, "standart" Clojure yapısından daha karmaşıktır:

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

bu, Leiningen projelerinin ve Clojure'un kendisinin varsayılanıdır .


Haritalar anahtar eşleştirme için Clojure's = yerine Java's equals () kullanır

İlk olarak IRC'de chouser tarafından bildirilen Java's equals () ' ın bu kullanımı bazı sezgisel olmayan sonuçlara yol açar:

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

Her iki yana Tamsayı ve Uzun 1 örneklerini varsayılan olarak aynı basılır, harita herhangi bir değer döndürüyor değil neden algılamak için zor olabilir. Bu, özellikle anahtarınızı, belki de size haber vermeden uzun bir süre döndüren bir işlevden geçirdiğinizde doğrudur.

Haritaların java.util.Map arayüzüne uyması için Clojure's = yerine Java's equals () kullanmanın gerekli olduğu unutulmamalıdır .


Ben kullanıyorum Clojure Programlama Stuart Halloway tarafından Pratik Clojure Luke VanderHart tarafından ve sayısız Clojure hacker yardımıyla IRC benim cevaplar boyunca yardım ve posta listesine.


1
Okuyucu makrolarının tümü normal işlev sürümüne sahiptir. Yapabilirsin (#(hash-set %1 %2) :a 1)ya da bu durumda (hash-set :a 1).
Brian Carper

2
Ayrıca, kimliğe sahip ek parantezleri de 'kaldırabilirsiniz': (# (kimlik {% 1% 2}): a 1)

1
Ayrıca kullanabilirsiniz do: (#(do {%1 %2}) :a 1).
Michał Marczyk

@ Michał - Bu çözümü öncekiler kadar sevmiyorum çünkü do bir yan etkinin olduğunu ima ediyor, aslında burada durum böyle değil.
Robert Campbell

@ rrc7cz: Eh, gerçekte, orada hiç burada isimsiz işlev kullanmaya gerek kullanarak beri var hash-map(olduğu gibi doğrudan (hash-map :a 1)ya da (map hash-map keys vals)daha okunabilir) ve bu özel bir şey anlamına gelmez ve henüz-as-adlandırılmış işlevi Uygulanmayan gerçekleşiyor (bunun kullanımı #(...)ima ediyor, buluyorum). Aslında, anonim fns'yi aşırı kullanmak kendi başına düşünülmesi gereken bir şeydir. :-) OTOH, bazen doyan etkisiz süper kısa anonim işlevler kullanıyorum ... Tek bakışta oldukları açık olma eğilimindedir. Bir zevk meselesi sanırım.
Michał Marczyk

42

Tembel sıraların değerlendirilmesini zorlamayı unutmak

Tembel seq'ler siz değerlendirilmelerini istemediğiniz sürece değerlendirilmez. Bunun bir şeyler yazdırmasını bekleyebilirsiniz, ancak öyle değildir.

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

mapO tembel, çünkü sessizce atılır oluyor, değerlendirilir asla. Sen birini kullanmak zorunda doseq, dorun, doallyan etkiler için tembel dizilerin değerlendirilmesini zorlamak için vb.

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

mapREPL türünde çıplak kullanmak işe yarıyor gibi görünür, ancak yalnızca REPL, tembel sıraların değerlendirilmesini zorladığı için işe yarar. Bu, hatanın fark edilmesini daha da zorlaştırabilir, çünkü kodunuz REPL'de çalışır ve bir kaynak dosyadan veya bir işlevin içinde çalışmaz.

user=> (map println [:foo :bar])
(:foo
:bar
nil nil)

1
+1. Bu ancak bir daha sinsi bir şekilde, beni ısıran: Ben değerlendirirken (map ...)içinden (binding ...)ve yeni bağlanma değerleri geçerli değildir sebebini merak.
Alex B

20

Ben Clojure noob'um. Daha ileri düzey kullanıcıların daha ilginç sorunları olabilir.

sonsuz tembel diziler yazdırmaya çalışıyor.

Tembel dizilerimle ne yaptığımı biliyordum, ancak hata ayıklama amacıyla, ne yazdırdığımı geçici olarak unuttuğum için bazı yazdırma / prn / pr çağrıları ekledim. Komik, neden bilgisayarım tamamen kapandı?

Clojure'u zorunlu olarak programlamaya çalışmak.

Çok sayıda refs veya atoms oluşturmak ve sürekli olarak durumlarıyla karıştıran kod yazmak için bazı cazibeler var . Bu yapılabilir, ancak uygun değildir. Ayrıca performansı düşük olabilir ve nadiren birden çok çekirdekten yararlanabilir.

Clojure'u% 100 işlevsel olarak programlamaya çalışıyor.

Bunun ters tarafı: Bazı algoritmalar gerçekten biraz değiştirilebilir durum ister. Her ne pahasına olursa olsun değişken durumdan dini olarak kaçınmak, yavaş veya garip algoritmalara neden olabilir. Karar vermek yargı ve biraz deneyim gerektirir.

Java'da çok şey yapmaya çalışıyorum.

Java'ya ulaşmak çok kolay olduğu için, Clojure'u Java çevresinde bir betik dili sarmalayıcı olarak kullanmak bazen cazip gelebilir. Java kitaplığı işlevselliğini kullanırken kesinlikle bunu tam olarak yapmanız gerekecektir, ancak (örneğin) Java'da veri yapılarını korumanın veya Clojure'da iyi eşdeğerleri olan koleksiyonlar gibi Java veri türlerini kullanmanın pek bir anlamı yoktur.


13

Daha önce bahsedilen birçok şey. Sadece bir tane daha ekleyeceğim.

Clojure eğer davranır Java Boole 's Değer false olsa bile geçerlidir her zaman olduğu gibi nesneleri. Dolayısıyla, bir java Boolean değeri döndüren bir java arazi işleviniz varsa, doğrudan (if java-bool "Yes" "No") değil , doğrudan kontrol ettiğinizden emin olun (if (boolean java-bool) "Yes" "No").

Veritabanı boole alanlarını java Boolean nesneleri olarak döndüren clojure.contrib.sql kitaplığı ile buna yakıldım.


8
Bunun (if java.lang.Boolean/FALSE (println "foo"))foo yazmadığını unutmayın. (if (java.lang.Boolean. "false") (println "foo")), oysa (if (boolean (java.lang.Boolean "false")) (println "foo"))değil ... Gerçekten kafa karıştırıcı!
Michał Marczyk

Clojure 1.4.0'da beklendiği gibi çalışıyor gibi görünüyor: (assert (=: false (eğer Boolean / FALSE: true: false)))
Jakub Holý

Ayrıca son zamanlarda (filter: mykey coll) yaparken de buna yakalanmıştım, burada: mykey's değerleri burada Booleans - Clojure tarafından oluşturulan koleksiyonlarda beklendiği gibi çalışıyor, ancak varsayılan Java serileştirmesi kullanılarak serileştirildiğinde serileştirilmemiş koleksiyonlarla DEĞİL - çünkü bu Booleanlar serileştirilmemiş hale getirildi yeni Boolean () ve ne yazık ki (yeni Boolean (doğru)! = java.lang.Boolean / DOĞRU)
Hendekagon

1
Clojure'deki Boole değerlerinin temel kurallarını hatırlayın - nilve falseyanlıştır ve diğer her şey doğrudur. Java Booleandeğildir nilve değildir false(çünkü bir nesne olduğu için), dolayısıyla davranış tutarlıdır.
erikprice

13

Başınızı döngülerde tutmak.
İlk öğeye referans tutarken potansiyel olarak çok büyük veya sonsuz, tembel bir dizinin öğeleri üzerinde döngü yaparsanız belleğinizin tükenme riski vardır.

TCO olmadığını unutmak.
Düzenli kuyruk aramaları yığın alanını tüketir ve dikkatli değilseniz taşırlar. Clojure sahiptir 'recurve 'trampolineoptimize edilmiş kuyruk çağrılar diğer dillerde kullanılacak vakaların çoğu işlemek için, ancak bu teknikler kasten uygulanmak zorundadır.

Oldukça tembel olmayan diziler.
İle 'lazy-seqveya 'lazy-cons(veya daha yüksek seviyeli tembel API'ler üzerine inşa ederek) tembel bir dizi oluşturabilirsiniz , ancak 'vecdiziyi gerçekleştiren başka bir işlevin içine sararsanız veya geçirirseniz, artık tembel olmayacaktır. Hem yığın hem de yığın bununla aşılabilir.

Değişken şeyleri referanslara koymak.
Teknik olarak bunu yapabilirsiniz, ancak yalnızca referansın kendisindeki nesne referansı STM tarafından yönetilir - atıfta bulunulan nesne ve alanları değil (bunlar değişmez ve diğer referansları göstermedikçe). Bu nedenle, mümkün olduğunda, referanslarda yalnızca değişmez nesneleri tercih edin. Aynı şey atomlar için de geçerli.


4
yaklaşan geliştirme dalı, yerel olarak erişilemez hale geldiklerinde bir işlevdeki nesnelere yapılan referansları silerek ilk öğeyi azaltma yolunda uzun bir yol kat ediyor.
Arthur Ulfeldt

9

loop ... recurharitanın işleyeceği sıraları işlemek için kullanmak .

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

vs.

(map do-stuff data)

Harita işlevi (en son dalda) yığın dizileri ve diğer birçok optimizasyonu kullanır. Ayrıca, bu işlev sıkça çalıştırıldığı için, Hotspot JIT genellikle onu optimize eder ve herhangi bir "ısınma süresi" olmadan çalışmaya hazır hale getirir.


1
Bu iki versiyon aslında eşdeğer değildir. Sizin workişlevi eşdeğerdir (doseq [item data] (do-stuff item)). (Bunun yanı sıra, işte bu döngü asla bitmez.)
kotarak

evet, ilki, argümanlarında tembelliği kırar. ortaya çıkan sıra, artık tembel bir sıra olmasa da aynı değerlere sahip olacaktır.
Arthur Ulfeldt

+1! Yalnızca tüm bunların mapve / veya kullanılarak genelleştirilebileceği başka bir gün bulmak için çok sayıda küçük özyinelemeli işlev yazdım reduce.
mike3996

5

Koleksiyon türlerinin bazı işlemler için farklı davranışları vardır:

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]

Dizelerle çalışmak kafa karıştırıcı olabilir (hala tam olarak anlamıyorum). Spesifik olarak dizeler, dizi işlevleri üzerlerinde çalışsa da, karakter dizileriyle aynı değildir:

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

Bir dizeyi geri almak için yapmanız gerekenler:

user=> (apply str (filter #(> (int %) 96) "abcdABCDefghEFGH"))
"abcdefgh"

3

çok fazla parantez, özellikle içinde NPE ile sonuçlanan void java yöntemi çağrıldığında

public void foo() {}

((.foo))

iç parantezler sıfır olarak değerlendirildiğinden, dış parantezlerden NPE ile sonuçlanır.

public int bar() { return 5; }

((.bar)) 

daha kolay hata ayıklamayla sonuçlanır:

java.lang.Integer cannot be cast to clojure.lang.IFn
  [Thrown class java.lang.ClassCastException]
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.