Typowe błędy programistyczne, których deweloperzy Clojure powinni unikać [zamknięte]

92

Jakie są typowe błędy popełniane przez programistów Clojure i jak możemy ich uniknąć?

Na przykład; nowicjusze w Clojure myślą, że ta contains?funkcja działa tak samo jak java.util.Collection#contains. Jednak contains?będzie działać podobnie tylko wtedy, gdy jest używany z indeksowanymi zbiorami, takimi jak mapy i zestawy, i szukasz danego klucza:

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

W przypadku użycia z kolekcjami indeksowanymi numerycznie (wektory, tablice) sprawdza contains? tylko , czy dany element należy do prawidłowego zakresu indeksów (od zera):

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

Jeśli podana lista, contains?nigdy nie zwróci true.

fogus
źródło
4
Do Twojej wiadomości, dla tych deweloperów Clojure, którzy szukają java.util.Collection # zawiera funkcjonalność typu, sprawdź clojure.contrib.seq-utils / includes? Z dokumentów: Użycie: (obejmuje? Coll x). Zwraca wartość true, jeśli coll zawiera coś równego (z =) x, w czasie liniowym.
Robert Campbell
11
Wygląda na to, że przegapiłeś fakt, że te pytania są na Wiki Społeczności
3
Uwielbiam to, że pytanie o Perla po prostu nie pasuje do wszystkich innych :)
Ether
8
Twórcom Clojure, którzy szukają zawartości, radziłbym nie postępować zgodnie z radą rcampbell. seq-utils od dawna jest przestarzałe i ta funkcja nigdy nie była przydatna. Możesz użyć somefunkcji Clojure lub, jeszcze lepiej, po prostu użyć containssamej siebie. Kolekcje Clojure wdrażają java.util.Collection. (.contains [1 2 3] 2) => true
Rayne

Odpowiedzi:

70

Dosłowne ósemki

W pewnym momencie czytałem w macierzy, w której zastosowano zera wiodące, aby zachować właściwe wiersze i kolumny. Matematycznie jest to poprawne, ponieważ wiodące zero oczywiście nie zmienia podstawowej wartości. Próby zdefiniowania zmiennej za pomocą tej macierzy zakończyłyby się jednak tajemniczym niepowodzeniem z:

java.lang.NumberFormatException: Invalid number: 08

co całkowicie mnie zaskoczyło. Powodem jest to, że Clojure traktuje dosłowne liczby całkowite z wiodącymi zerami jako ósemki, aw ósemkowym nie ma liczby 08.

Powinienem również wspomnieć, że Clojure obsługuje tradycyjne wartości szesnastkowe Java poprzez prefiks 0x . Możesz również użyć dowolnej podstawy od 2 do 36, używając notacji „podstawa + r + wartość”, na przykład 2r101010 lub 36r16, które mają 42 podstawy dziesięć.


Próba zwrócenia literałów w anonimowym literale funkcji

To działa:

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

więc wierzyłem, że to również zadziała:

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

ale zawodzi z:

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

ponieważ makro czytnika # () zostaje rozwinięte do

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

z literałem mapy w nawiasach. Ponieważ jest to pierwszy element, jest traktowany jako funkcja (którą w rzeczywistości jest mapa literału), ale nie są dostarczane żadne wymagane argumenty (takie jak klucz). Podsumowując, anonimowy literał funkcji nie rozwija się do

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

dlatego nie możesz mieć żadnej wartości dosłownej ([],: a, 4,%) jako treści funkcji anonimowej.

W komentarzach podano dwa rozwiązania. Brian Carper sugeruje użycie konstruktorów implementacji sekwencji (array-map, hash-set, vector) w następujący sposób:

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

podczas gdy Dan pokazuje, że możesz użyć funkcji identity, aby rozpakować zewnętrzny nawias:

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

Sugestia Briana doprowadza mnie do kolejnego błędu ...


Myślenie, że mapa hash lub tablica-mapa określa niezmienną implementację konkretnej mapy

Rozważ następujące:

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

Choć na ogół nie będą musieli się martwić o realizacji betonowej z mapą Clojure, należy wiedzieć, że funkcje, które rosną mapa - jak doc lub powiązaniu - może podjąć PersistentArrayMap i zwracają PersistentHashMap , który wykonuje się szybciej na większych mapach.


Używanie funkcji jako punktu rekurencji zamiast pętli w celu zapewnienia początkowych powiązań

Kiedy zaczynałem, napisałem wiele funkcji, takich jak:

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

Kiedy w rzeczywistości pętla byłaby bardziej zwięzła i idiomatyczna dla tej konkretnej funkcji:

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

Zauważ, że zastąpiłem pusty argument, treść funkcji „domyślny konstruktor” (p3 775147 600851475143 3) pętlą + początkowe wiązanie. Powtarzające się teraz na ponowne powiązanie wiązania pętli (a fn parametrami) i powraca do punktu rekursji (pętla zamiast FN).


Nawiązanie do zmiennych „widmowych”

Mówię o typie var, który możesz zdefiniować za pomocą REPL - podczas programowania eksploracyjnego - a następnie nieświadomie odwołać się do źródła. Wszystko działa dobrze, dopóki nie przeładujesz przestrzeni nazw (być może przez zamknięcie edytora), a później odkryjesz kilka niezwiązanych symboli, do których odwołuje się Twój kod. Dzieje się tak również często podczas refaktoryzacji, przenoszenia zmiennej z jednej przestrzeni nazw do drugiej.


Traktowanie rozumienia listy for jako imperatywu pętli

Zasadniczo tworzysz leniwą listę na podstawie istniejących list, zamiast po prostu wykonywać kontrolowaną pętlę. Clojure za doseq jest rzeczywiście bardziej analogiczne do nadrzędnych foreach pętli konstrukcjami.

Jednym z przykładów ich różnic jest możliwość filtrowania, po których elementach iterują, przy użyciu dowolnych predykatów:

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)

Innym sposobem, w jaki się różnią, jest to, że mogą operować na nieskończonych leniwych sekwencjach:

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

Mogą również obsługiwać więcej niż jedno wyrażenie wiążące, iterując najpierw po skrajnym prawym wyrażeniu i działając w lewo:

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

Jest też bez przerwy lub kontynuować , aby zakończyć przedwcześnie.


Nadużywanie struktur

Pochodzę z OOPish, więc kiedy zacząłem Clojure, mój mózg wciąż myślał w kategoriach przedmiotów. Zacząłem modelować wszystko jako strukturę, ponieważ jej grupowanie „członków”, jakkolwiek luźne, zapewniało mi poczucie komfortu. W rzeczywistości struktury powinny być głównie traktowane jako optymalizacja; Clojure udostępni klucze i niektóre informacje wyszukiwania, aby oszczędzać pamięć. Można dodatkowo zoptymalizować je poprzez zdefiniowanie akcesorów , aby przyspieszyć proces klucza odnośnika.

Ogólnie rzecz biorąc, używanie struktury na mapie nie daje nic poza wydajnością, więc dodatkowa złożoność może nie być tego warta.


Korzystanie z nieugerowanych konstruktorów BigDecimal

Potrzebowałem dużo BigDecimals i pisałem brzydki kod w ten sposób:

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

podczas gdy w rzeczywistości Clojure obsługuje literały BigDecimal, dodając M do liczby:

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

Używanie wersji z cukrem eliminuje wiele wzdęć. W komentarzach twils wspomniał, że możesz również użyć funkcji bigdec i bigint, aby być bardziej zrozumiałym, ale zachować zwięzłość.


Korzystanie z konwersji nazewnictwa pakietów Java dla przestrzeni nazw

W rzeczywistości nie jest to błąd sam w sobie, ale raczej coś, co jest sprzeczne z idiomatyczną strukturą i nazewnictwem typowego projektu Clojure. Mój pierwszy istotny projekt Clojure miał deklaracje przestrzeni nazw - i odpowiadające im struktury folderów - w ten sposób:

(ns com.14clouds.myapp.repository)

co rozdęło moje w pełni kwalifikowane odwołania do funkcji:

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

Aby jeszcze bardziej skomplikować sprawę, użyłem standardowej struktury katalogów Maven :

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

który jest bardziej złożony niż „standardowa” struktura Clojure:

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

co jest domyślne dla projektów Leiningen i samego Clojure .


Mapy używają równości () Java zamiast Clojure = do dopasowywania kluczy

Pierwotnie zgłoszone przez chousera na IRC , to użycie metody equals () w Javie prowadzi do pewnych nieintuicyjnych wyników:

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

Ponieważ zarówno Integer, jak i Long instancji 1 są domyślnie drukowane tak samo, może być trudno wykryć, dlaczego mapa nie zwraca żadnych wartości. Jest to szczególnie prawdziwe, gdy przekazujesz klucz przez funkcję, która być może bez Twojej wiedzy zwraca długi.

Należy zauważyć, że przy użyciu Java equals () zamiast Clojure w = ma zasadnicze znaczenie dla mapy aby były zgodne z interfejsem java.util.Map.


Używam Programming Clojure autorstwa Stuarta Hallowaya, Practical Clojure autorstwa Luke VanderHart oraz pomocy niezliczonych hakerów Clojure na IRC i liście mailingowej, aby pomóc w udzieleniu odpowiedzi.

rcampbell
źródło
1
Wszystkie makra czytnika mają normalną wersję funkcji. Możesz to zrobić (#(hash-set %1 %2) :a 1)lub w tym przypadku (hash-set :a 1).
Brian Carper
2
Możesz także „usunąć” dodatkowe nawiasy z tożsamością: (# (identity {% 1% 2}): a 1)
1
Można również użyć do: (#(do {%1 %2}) :a 1).
Michał Marczyk
@ Michał - Nie podoba mi się to rozwiązanie tak bardzo, jak poprzednie, ponieważ nie sugeruje, że ma miejsce efekt uboczny, podczas gdy w rzeczywistości tak nie jest w tym przypadku.
Robert Campbell
@ rrc7cz: Cóż, w rzeczywistości w ogóle nie ma potrzeby używania tutaj funkcji anonimowej, ponieważ użycie hash-mapbezpośrednie (jak w (hash-map :a 1)lub (map hash-map keys vals)) jest bardziej czytelne i nie oznacza, że ​​w nazwanej funkcji jest coś specjalnego i jeszcze nie zaimplementowanego ma miejsce (co #(...)sugeruje użycie tagu). W rzeczywistości nadużywanie anonimowych fns jest problemem samym w sobie. :-) OTOH, czasami używam dobardzo zwięzłych, anonimowych funkcji, które są wolne od skutków ubocznych ... Wydaje się oczywiste, że są one widoczne na pierwszy rzut oka. Chyba kwestia gustu.
Michał Marczyk
42

Zapominanie o wymuszaniu oceny leniwych ciągów

Leniwe sekwencje nie są oceniane, chyba że poprosisz je o ocenę. Możesz oczekiwać, że coś wydrukuje, ale tak się nie dzieje.

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

mapNie jest oceniana, to dyskretnie wyrzucić, bo to leniwy. Musisz użyć jednego z doseq, dorun, doallitd., Aby wymusić ocenę leniwych sekwencji do skutków ubocznych.

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

Używanie goła mapw REPL wygląda tak, jakby to działało, ale działa tylko dlatego, że REPL wymusza ocenę samych leniwych sekwencji. Może to sprawić, że błąd będzie jeszcze trudniejszy do zauważenia, ponieważ twój kod działa w REPL i nie działa z pliku źródłowego lub wewnątrz funkcji.

user=> (map println [:foo :bar])
(:foo
:bar
nil nil)
Brian Carper
źródło
1
+1. Ugryzło mnie to, ale w bardziej podstępny sposób: oceniałem (map ...)od wewnątrz (binding ...)i zastanawiałem się, dlaczego nowe wiążące wartości nie mają zastosowania.
Alex B
20

Jestem noobem Clojure. Bardziej zaawansowani użytkownicy mogą mieć ciekawsze problemy.

próbując wydrukować nieskończone leniwe sekwencje.

Wiedziałem, co robię z moimi leniwymi sekwencjami, ale dla celów debugowania wstawiłem kilka wywołań print / prn / pr, tymczasowo zapomniawszy, co robię. Zabawne, dlaczego mój komputer się zawiesił?

próbując bezwzględnie zaprogramować Clojure.

Istnieje pokusa, aby stworzyć całą masę refS lub atomS i napisać kod, który nieustannie miesza się z ich stanem. Można to zrobić, ale nie jest to dobre dopasowanie. Może również mieć słabą wydajność i rzadko korzystać z wielu rdzeni.

próbując zaprogramować Clojure w 100% funkcjonalnie.

Druga strona tego: niektóre algorytmy naprawdę chcą trochę zmiennego stanu. Religijne unikanie stanu zmiennego za wszelką cenę może skutkować powolnymi lub niewygodnymi algorytmami. Podjęcie decyzji wymaga osądu i odrobiny doświadczenia.

próbując robić za dużo w Javie.

Ponieważ tak łatwo jest dotrzeć do Javy, czasami kusi użycie Clojure jako opakowania języka skryptowego wokół Javy. Z pewnością będziesz musiał zrobić to dokładnie podczas korzystania z funkcjonalności biblioteki Java, ale nie ma sensu (np.) Utrzymywać struktur danych w Javie lub używać typów danych Java, takich jak kolekcje, dla których istnieją dobre odpowiedniki w Clojure.

Carl Smotricz
źródło
13

Wiele rzeczy już wspomniano. Dodam jeszcze jeden.

Clojure if traktuje obiekty Java Boolean zawsze jako prawdziwe, nawet jeśli ma wartość false. Więc jeśli masz funkcję java land, która zwraca wartość logiczną java, upewnij się, że nie sprawdzasz jej bezpośrednio, (if java-bool "Yes" "No") ale raczej (if (boolean java-bool) "Yes" "No").

Zostałem spalony przez to z biblioteką clojure.contrib.sql, która zwraca pola boolowskie bazy danych jako obiekty logiczne java.

Vagif Verdi
źródło
8
Zauważ, że (if java.lang.Boolean/FALSE (println "foo"))nie drukuje foo. (if (java.lang.Boolean. "false") (println "foo"))robi jednak, (if (boolean (java.lang.Boolean "false")) (println "foo"))ale nie ... Doprawdy dość mylące!
Michał Marczyk
Wygląda na to, że działa zgodnie z oczekiwaniami w Clojure 1.4.0: (assert (=: false (if Boolean / FALSE: true: false)))
Jakub Holý
Zostałem również spalony przez ten ostatnio podczas wykonywania (filter: mykey coll), gdzie: wartości mykey, gdzie Booleans - działa zgodnie z oczekiwaniami z kolekcjami utworzonymi przez Clojure, ale NIE z kolekcjami zdeserializowanymi, gdy serializowany przy użyciu domyślnej serializacji Java - ponieważ te wartości Booleans są deserializowane as new Boolean () i niestety (new Boolean (true)! = java.lang.Boolean / TRUE)
Hendekagon.
1
Pamiętaj tylko o podstawowych zasadach wartości logicznych w Clojure - nili falsesą one fałszywe, a wszystko inne jest prawdą. Java Booleannie jest nili nie jest false(ponieważ jest obiektem), więc zachowanie jest spójne.
erikprice
13

Trzymaj głowę w pętli.
Ryzykujesz, że zabraknie pamięci, jeśli zapętlisz elementy potencjalnie bardzo dużej lub nieskończonej, leniwej sekwencji, zachowując odniesienie do pierwszego elementu.

Zapominając o braku TCO.
Regularne wywołania ogonowe zajmują miejsce na stosie i przepełnią się, jeśli nie będziesz ostrożny. Clojure ma 'recuri 'trampolineobsługuje wiele przypadków, w których zoptymalizowane wywołania ogonowe byłyby używane w innych językach, ale techniki te muszą być celowo stosowane.

Niezupełnie leniwe sekwencje.
Możesz zbudować leniwą sekwencję za pomocą 'lazy-seqlub 'lazy-cons(lub budując na leniwych interfejsach API wyższego poziomu), ale jeśli zawiniesz ją 'veclub przepuścisz przez jakąś inną funkcję, która realizuje sekwencję, nie będzie już leniwa. Może to spowodować przepełnienie zarówno stosu, jak i sterty.

Umieszczanie zmiennych rzeczy w ref.
Technicznie możesz to zrobić, ale tylko odniesienie do obiektu w samym ref jest regulowane przez STM - a nie obiekt, do którego odnosi się odwołanie i jego pola (chyba że są one niezmienne i wskazują na inne odniesienia). Więc jeśli to możliwe, preferuj tylko niezmienne obiekty w referencjach. To samo dotyczy atomów.

Chris Vest
źródło
4
nadchodząca gałąź rozwoju idzie daleko w kierunku zredukowania pierwszego elementu, usuwając odniesienia do obiektów w funkcji, gdy staną się lokalnie nieosiągalne.
Arthur Ulfeldt
9

użycie loop ... recurdo przetwarzania sekwencji, gdy zrobi to mapa.

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

vs.

(map do-stuff data)

Funkcja map (w najnowszej gałęzi) używa fragmentów sekwencji i wielu innych optymalizacji. Ponadto, ponieważ ta funkcja jest często uruchamiana, Hotspot JIT zwykle ma ją zoptymalizowaną i gotową do pracy bez „czasu na rozgrzanie”.

Arthur Ulfeldt
źródło
1
Te dwie wersje w rzeczywistości nie są równoważne. Twoja workfunkcja jest równoważna (doseq [item data] (do-stuff item)). (Poza tym ta pętla w pracy nigdy się nie kończy.)
kotarak
tak, pierwszy przełamuje lenistwo w swoich argumentach. wynikowy ciąg będzie miał te same wartości, chociaż nie będzie już leniwym ciągiem.
Arthur Ulfeldt
+1! Napisałem wiele małych funkcji rekurencyjnych tylko po to, aby znaleźć inny dzień, w którym można je uogólnić za pomocą mapi / lub reduce.
mike3996
5

Typy kolekcji mają różne zachowania dla niektórych operacji:

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]

Praca ze stringami może być myląca (nadal ich nie rozumiem). W szczególności łańcuchy nie są tym samym, co sekwencje znaków, mimo że działają na nich funkcje sekwencji:

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

Aby odzyskać ciąg, musisz zrobić:

user=> (apply str (filter #(> (int %) 96) "abcdABCDefghEFGH"))
"abcdefgh"
Matt Fenwick
źródło
3

zbyt wiele parantez, szczególnie z wywołaniem metody void java wewnątrz, co skutkuje NPE:

public void foo() {}

((.foo))

daje w wyniku NPE z zewnętrznych parantez, ponieważ wewnętrzne parantezy dają zero.

public int bar() { return 5; }

((.bar)) 

skutkuje łatwiejszym do debugowania:

java.lang.Integer cannot be cast to clojure.lang.IFn
  [Thrown class java.lang.ClassCastException]
miaubiz
źródło