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.
some
funkcji Clojure lub, jeszcze lepiej, po prostu użyćcontains
samej siebie. Kolekcje Clojure wdrażająjava.util.Collection
.(.contains [1 2 3] 2) => true
Odpowiedzi:
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:
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 :
który jest bardziej złożony niż „standardowa” struktura Clojure:
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.
źródło
(#(hash-set %1 %2) :a 1)
lub w tym przypadku(hash-set :a 1)
.do
:(#(do {%1 %2}) :a 1)
.hash-map
bezpoś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żywamdo
bardzo 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.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
map
Nie jest oceniana, to dyskretnie wyrzucić, bo to leniwy. Musisz użyć jednego zdoseq
,dorun
,doall
itd., 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
map
w 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)
źródło
(map ...)
od wewnątrz(binding ...)
i zastanawiałem się, dlaczego nowe wiążące wartości nie mają zastosowania.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ę
ref
S lubatom
S 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.
źródło
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.
źródło
(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!nil
ifalse
są one fałszywe, a wszystko inne jest prawdą. JavaBoolean
nie jestnil
i nie jestfalse
(ponieważ jest obiektem), więc zachowanie jest spójne.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
'recur
i'trampoline
obsł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-seq
lub'lazy-cons
(lub budując na leniwych interfejsach API wyższego poziomu), ale jeśli zawiniesz ją'vec
lub 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.
źródło
użycie
loop ... recur
do 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”.
źródło
work
funkcja jest równoważna(doseq [item data] (do-stuff item))
. (Poza tym ta pętla w pracy nigdy się nie kończy.)map
i / lubreduce
.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"
źródło
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:
źródło