Häufige Programmierfehler für Clojure-Entwickler, um [geschlossen] zu vermeiden

92

Was sind einige häufige Fehler von Clojure-Entwicklern und wie können wir sie vermeiden?

Beispielsweise; Neulinge bei Clojure denken, dass die contains?Funktion genauso funktioniert wie java.util.Collection#contains. Funktioniert jedoch contains?nur ähnlich, wenn es mit indizierten Sammlungen wie Karten und Sets verwendet wird und Sie nach einem bestimmten Schlüssel suchen:

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

Bei Verwendung mit numerisch indizierten Sammlungen (Vektoren, Arrays) wird contains? nur überprüft, ob das angegebene Element innerhalb des gültigen Indexbereichs liegt (nullbasiert):

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

Wenn eine Liste angegeben wird, contains?wird niemals true zurückgegeben.

Nebel
quelle
4
Nur zu Ihrer Information, für diejenigen Clojure-Entwickler, die nach java.util suchen. Die Sammlungsnummer enthält Typfunktionalität. Schauen Sie sich clojure.contrib.seq-utils / includes an. Aus den Dokumenten: Verwendung: (enthält? Coll x). Gibt true zurück, wenn coll in linearer Zeit etwas enthält, das (mit =) x entspricht.
Robert Campbell
11
Sie scheinen die Tatsache übersehen zu haben , dass diese Fragen sind Community Wiki
3
Ich liebe es, wie die Perl-Frage einfach nicht mit allen anderen Schritt halten muss :)
Ether
8
Für Clojure-Entwickler, die nach Contains suchen, würde ich empfehlen, den Ratschlägen von rcampbell nicht zu folgen. seq-utils ist längst veraltet und diese Funktion war anfangs nie nützlich. Sie können die someFunktion von Clojure verwenden oder, noch besser, sich containsselbst verwenden. Clojure-Sammlungen implementieren java.util.Collection. (.contains [1 2 3] 2) => true
Rayne

Antworten:

70

Wörtliche Oktale

Irgendwann las ich in einer Matrix, in der führende Nullen verwendet wurden, um die richtigen Zeilen und Spalten zu erhalten. Mathematisch ist dies richtig, da die führende Null den zugrunde liegenden Wert offensichtlich nicht ändert. Versuche, eine Var mit dieser Matrix zu definieren, würden jedoch auf mysteriöse Weise scheitern mit:

java.lang.NumberFormatException: Invalid number: 08

das hat mich total verblüfft. Der Grund dafür ist, dass Clojure Literal-Integer-Werte mit führenden Nullen als Oktale behandelt und es keine Nummer 08 im Oktal gibt.

Ich sollte auch erwähnen, dass Clojure traditionelle Java-Hexadezimalwerte über das Präfix 0x unterstützt . Sie können auch eine beliebige Basis zwischen 2 und 36 verwenden, indem Sie die Notation "base + r + value" verwenden, z. B. 2r101010 oder 36r16 (42 Basis zehn).


Es wird versucht, Literale in einem anonymen Funktionsliteral zurückzugeben

Das funktioniert:

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

Also glaubte ich, dass dies auch funktionieren würde:

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

aber es scheitert mit:

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

weil das # () Reader-Makro auf erweitert wird

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

mit dem Kartenliteral in Klammern. Da es das erste Element ist, wird es als Funktion behandelt (was eine Literal-Map tatsächlich ist), es werden jedoch keine erforderlichen Argumente (z. B. ein Schlüssel) bereitgestellt. Zusammenfassend lässt sich sagen, dass das anonyme Funktionsliteral nicht erweitert wird

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

Daher können Sie keinen Literalwert ([] ,: a, 4,%) als Hauptteil der anonymen Funktion haben.

In den Kommentaren wurden zwei Lösungen angegeben. Brian Carper schlägt vor, Sequenzimplementierungskonstruktoren (Array-Map, Hash-Set, Vektor) wie folgt zu verwenden:

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

während Dan zeigt, dass Sie die Identitätsfunktion verwenden können, um die äußere Klammer zu entpacken:

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

Brians Vorschlag bringt mich tatsächlich zu meinem nächsten Fehler ...


Der Gedanke, dass Hash-Map oder Array-Map die unveränderliche konkrete Map-Implementierung bestimmen

Folgendes berücksichtigen:

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

Während Sie sich im Allgemeinen nicht um die konkrete Implementierung einer Clojure-Karte kümmern müssen, sollten Sie wissen, dass Funktionen, die eine Karte vergrößern - wie assoc oder conj -, eine PersistentArrayMap verwenden und eine PersistentHashMap zurückgeben können , die bei größeren Karten eine schnellere Leistung erbringt.


Verwenden einer Funktion als Rekursionspunkt anstelle einer Schleife , um anfängliche Bindungen bereitzustellen

Als ich anfing, schrieb ich viele Funktionen wie diese:

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

In der Tat wäre die Schleife für diese bestimmte Funktion prägnanter und idiomatischer gewesen:

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

Beachten Sie, dass ich das leere Argument "Standardkonstruktor" -Funktionskörper (p3 775147 600851475143 3) durch eine Schleife + anfängliche Bindung ersetzt habe. Die RECUR nun erneut bindet die Loop - Bindungen (anstelle der Parameter fn) und springt zurück zum Rekursion Punkt (Schleife statt fn).


Referenzieren von "Phantom" -Vars

Ich spreche über die Art von var, die Sie mithilfe der REPL definieren könnten - während Ihrer explorativen Programmierung - und dann unwissentlich in Ihrer Quelle referenzieren. Alles funktioniert einwandfrei, bis Sie den Namespace neu laden (möglicherweise durch Schließen Ihres Editors) und später eine Reihe ungebundener Symbole entdecken, auf die im gesamten Code verwiesen wird. Dies tritt auch häufig auf, wenn Sie ein Varactoring durchführen und eine Variable von einem Namespace in einen anderen verschieben.


Behandeln Sie das For- Listen-Verständnis wie einen Imperativ für die Schleife

Im Wesentlichen erstellen Sie eine Lazy-Liste basierend auf vorhandenen Listen, anstatt einfach eine Regelschleife auszuführen. Clojure der doseq ist eigentlich mehr analog Imperativ foreach Schleifenkonstrukte.

Ein Beispiel dafür, wie unterschiedlich sie sind, ist die Möglichkeit, mithilfe beliebiger Prädikate zu filtern, über welche Elemente sie iterieren:

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)

Eine andere Art, wie sie sich unterscheiden, ist, dass sie mit unendlich faulen Sequenzen arbeiten können:

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

Sie können auch mehr als einen Bindungsausdruck verarbeiten, wobei sie zuerst über den Ausdruck ganz rechts iterieren und sich nach links arbeiten:

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

Es gibt auch keine Pause oder weiterhin vorzeitig zu beenden.


Überbeanspruchung von Strukturen

Ich komme aus einem OOPish-Umfeld. Als ich mit Clojure anfing, dachte mein Gehirn immer noch an Objekte. Ich fand mich dabei, alles als Struktur zu modellieren, weil ich mich durch die Gruppierung von "Mitgliedern", so locker sie auch sein mögen, wohl fühlte. In der Realität sollten Strukturen meist als Optimierung betrachtet werden. Clojure teilt die Schlüssel und einige Suchinformationen, um Speicherplatz zu sparen. Sie können sie weiter optimieren, indem Sie Accessoren definieren , um die Suche nach Schlüsseln zu beschleunigen.

Insgesamt erhalten Sie durch die Verwendung einer Struktur über einer Karte nichts außer der Leistung, sodass sich die zusätzliche Komplexität möglicherweise nicht lohnt.


Verwenden nicht empfohlener BigDecimal-Konstruktoren

Ich brauchte viele BigDecimals und schrieb so hässlichen Code:

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

Tatsächlich unterstützt Clojure BigDecimal-Literale, indem M an die Zahl angehängt wird :

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

Durch die Verwendung der gezuckerten Version wird ein Großteil des Aufblähens vermieden. In den Kommentaren erwähnte Twils , dass Sie auch die Funktionen bigdec und bigint verwenden können, um expliziter zu sein, aber dennoch präzise bleiben.


Verwenden der Namenskonvertierungen für Java-Pakete für Namespaces

Dies ist eigentlich kein Fehler an sich, sondern etwas, das gegen die idiomatische Struktur und Benennung eines typischen Clojure-Projekts verstößt. Mein erstes umfangreiches Clojure-Projekt hatte Namespace-Deklarationen - und entsprechende Ordnerstrukturen - wie folgt:

(ns com.14clouds.myapp.repository)

was meine vollqualifizierten Funktionsreferenzen aufgebläht hat:

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

Um die Sache noch komplizierter zu machen, habe ich eine Standard- Maven- Verzeichnisstruktur verwendet:

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

Das ist komplexer als die "Standard" Clojure-Struktur von:

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

Dies ist die Standardeinstellung von Leiningen- Projekten und Clojure selbst.


Karten verwenden Javas equals () anstelle von Clojures = für die Schlüsselübereinstimmung

Ursprünglich von Chouser im IRC gemeldet , führt diese Verwendung von Javas equals () zu einigen nicht intuitiven Ergebnissen:

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

Da sowohl Integer- als auch Long- Instanzen von 1 standardmäßig gleich gedruckt werden, kann es schwierig sein, festzustellen, warum Ihre Karte keine Werte zurückgibt. Dies gilt insbesondere dann, wenn Sie Ihren Schlüssel durch eine Funktion übergeben, die, möglicherweise ohne Ihr Wissen, eine lange Zeit zurückgibt.

Es ist zu beachten, dass die Verwendung von Java's equals () anstelle von Clojure's = wichtig ist, damit Maps der Schnittstelle java.util.Map entsprechen.


Ich verwende Programming Clojure von Stuart Halloway, Practical Clojure von Luke VanderHart und die Hilfe unzähliger Clojure-Hacker im IRC und die Mailingliste, um meine Antworten zu unterstützen.

rcampbell
quelle
1
Alle Reader-Makros haben eine normale Funktionsversion. Sie könnten tun (#(hash-set %1 %2) :a 1)oder in diesem Fall (hash-set :a 1).
Brian Carper
2
Sie können auch die zusätzlichen Klammern mit Identität 'entfernen': (# (Identität {% 1% 2}): a 1)
1
Sie können auch verwenden do: (#(do {%1 %2}) :a 1).
Michał Marczyk
@ Michał - Ich mag diese Lösung nicht so sehr wie die vorherigen, weil dies impliziert, dass ein Nebeneffekt auftritt, obwohl dies hier tatsächlich nicht der Fall ist.
Robert Campbell
@ rrc7cz: Nun, in Wirklichkeit ist es hier überhaupt nicht erforderlich, eine anonyme Funktion zu verwenden, da die hash-mapdirekte Verwendung (wie in (hash-map :a 1)oder (map hash-map keys vals)) besser lesbar ist und nicht impliziert, dass etwas Besonderes und bisher in einer benannten Funktion nicht implementiert ist findet statt (was die Verwendung von #(...)impliziert, finde ich). In der Tat ist die Überbeanspruchung anonymer FNS ein Problem, über das man an sich nachdenken muss. :-) OTOH, ich verwende manchmal doin sehr präzisen anonymen Funktionen, die frei von Nebenwirkungen sind ... Es ist offensichtlich, dass sie auf einen Blick sind. Eine Frage des Geschmacks, denke ich.
Michał Marczyk
42

Vergessen, die Auswertung von Lazy Seqs zu erzwingen

Lazy Seqs werden nur ausgewertet, wenn Sie sie zur Auswertung auffordern. Sie können erwarten, dass dies etwas druckt, aber es tut es nicht.

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

Das mapwird nie ausgewertet, es wird stillschweigend verworfen, weil es faul ist. Sie haben einen von verwenden doseq, dorun, doallusw. Bewertung der faulen Sequenzen für Nebenwirkungen zu erzwingen.

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

Die Verwendung eines Bare mapbei REPL sieht so aus, als würde es funktionieren, aber es funktioniert nur, weil die REPL die Auswertung von Lazy Seqs selbst erzwingt. Dies kann es noch schwieriger machen, den Fehler zu bemerken, da Ihr Code auf der REPL funktioniert und nicht aus einer Quelldatei oder innerhalb einer Funktion funktioniert.

user=> (map println [:foo :bar])
(:foo
:bar
nil nil)
Brian Carper
quelle
1
+1. Das hat mich gebissen, aber auf heimtückischere Weise: Ich habe (map ...)von innen heraus bewertet (binding ...)und mich gefragt, warum neue Bindungswerte nicht zutreffen.
Alex B
20

Ich bin ein Clojure Noob. Fortgeschrittene Benutzer haben möglicherweise interessantere Probleme.

versuchen, unendlich faule Sequenzen zu drucken.

Ich wusste, was ich mit meinen faulen Sequenzen machte, aber zu Debugging-Zwecken fügte ich einige print / prn / pr-Aufrufe ein, nachdem ich vorübergehend vergessen hatte, was ich druckte. Komisch, warum hat mein PC alle aufgelegt?

versuchen, Clojure unbedingt zu programmieren.

Es besteht die Versuchung, eine ganze Menge refs oder atoms zu erstellen und Code zu schreiben, der ständig mit ihrem Zustand in Konflikt gerät. Dies kann getan werden, aber es passt nicht gut. Es kann auch eine schlechte Leistung haben und selten von mehreren Kernen profitieren.

versuchen, Clojure 100% funktional zu programmieren.

Eine Kehrseite dazu: Einige Algorithmen wollen wirklich einen veränderlichen Zustand. Das religiöse Vermeiden eines veränderlichen Zustands um jeden Preis kann zu langsamen oder umständlichen Algorithmen führen. Es braucht Urteilsvermögen und ein bisschen Erfahrung, um die Entscheidung zu treffen.

versuchen, in Java zu viel zu tun.

Da es so einfach ist, Java zu erreichen, ist es manchmal verlockend, Clojure als Wrapper für Skriptsprachen in Java zu verwenden. Natürlich müssen Sie genau dies tun, wenn Sie Java-Bibliotheksfunktionen verwenden, aber es macht wenig Sinn, (z. B.) Datenstrukturen in Java zu verwalten oder Java-Datentypen wie Sammlungen zu verwenden, für die es in Clojure gute Entsprechungen gibt.

Carl Smotricz
quelle
13

Viele Dinge bereits erwähnt. Ich werde nur noch eine hinzufügen.

Clojure if behandelt Java-Boolesche Objekte immer als true, auch wenn der Wert false ist. Wenn Sie also eine Java-Land-Funktion haben, die einen Java-Booleschen Wert zurückgibt, stellen Sie sicher, dass Sie ihn nicht direkt, (if java-bool "Yes" "No") sondern überprüfen (if (boolean java-bool) "Yes" "No").

Dies hat mich mit der Bibliothek clojure.contrib.sql verbrannt, die boolesche Datenbankfelder als Java-Boolesche Objekte zurückgibt.

Vagif Verdi
quelle
8
Beachten Sie, dass (if java.lang.Boolean/FALSE (println "foo"))foo nicht gedruckt wird. (if (java.lang.Boolean. "false") (println "foo"))tut es aber, wohingegen (if (boolean (java.lang.Boolean "false")) (println "foo"))es nicht ... In der Tat ziemlich verwirrend!
Michał Marczyk
Es scheint wie erwartet in Clojure 1.4.0 zu funktionieren: (assert (=: false (wenn Boolean / FALSE: true: false))
Jakub Holý
Ich habe mich kürzlich auch von diesem verbrannt (filter: mykey coll), wo: mykeys Werte, bei denen Booleans - wie erwartet mit von Clojure erstellten Sammlungen funktioniert, aber NICHT mit deserialisierten Sammlungen, wenn sie mit der Standard-Java-Serialisierung serialisiert werden - weil diese Booleans deserialisiert sind als neuer Boolean () und leider (neuer Boolean (true)! = java.lang.Boolean / TRUE)
Hendekagon
1
Denken Sie nur an die Grundregeln der Booleschen Werte in Clojure - nilund falsesind falsch, und alles andere ist wahr. Ein Java Booleanist nicht nilund es ist nicht false(weil es ein Objekt ist), daher ist das Verhalten konsistent.
Erikpreis
13

Halte deinen Kopf in Schleifen.
Es besteht die Gefahr, dass Ihnen der Speicher ausgeht, wenn Sie die Elemente einer möglicherweise sehr großen oder unendlichen, faulen Sequenz durchlaufen und dabei auf das erste Element verweisen.

Vergessen, dass es keine TCO gibt.
Regelmäßige Tail-Calls belegen Stapelspeicher und laufen über, wenn Sie nicht vorsichtig sind. Clojure hat 'recurund muss 'trampolineviele der Fälle behandeln, in denen optimierte Tail-Calls in anderen Sprachen verwendet werden, aber diese Techniken müssen absichtlich angewendet werden.

Nicht ganz faule Sequenzen.
Sie können eine Lazy-Sequenz mit 'lazy-seqoder 'lazy-cons(oder indem Sie auf Lazy-APIs höherer Ebene aufbauen) erstellen. Wenn Sie sie jedoch einschließen 'vecoder durch eine andere Funktion übergeben, die die Sequenz realisiert, ist sie nicht mehr Lazy. Dadurch können sowohl der Stapel als auch der Heap überflogen werden.

Veränderliche Dinge in Refs setzen.
Sie können dies technisch tun, aber nur die Objektreferenz in der Referenz selbst wird vom STM gesteuert - nicht das referenzierte Objekt und seine Felder (es sei denn, sie sind unveränderlich und verweisen auf andere Referenzen). Ziehen Sie es daher nach Möglichkeit vor, nur unveränderliche Objekte in Refs zu verwenden. Gleiches gilt für Atome.

Chris Vest
quelle
4
Der bevorstehende Entwicklungszweig trägt wesentlich dazu bei, das erste Element zu reduzieren, indem Verweise auf Objekte in einer Funktion gelöscht werden, sobald sie lokal nicht mehr erreichbar sind.
Arthur Ulfeldt
9

Verwenden Sie loop ... recur, um Sequenzen zu verarbeiten, wenn die Karte ausreicht.

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

vs.

(map do-stuff data)

Die Kartenfunktion (im neuesten Zweig) verwendet Blocksequenzen und viele andere Optimierungen. Da diese Funktion häufig ausgeführt wird, ist sie beim Hotspot JIT normalerweise optimiert und ohne "Aufwärmzeit" einsatzbereit.

Arthur Ulfeldt
quelle
1
Diese beiden Versionen sind eigentlich nicht gleichwertig. Ihre workFunktion entspricht (doseq [item data] (do-stuff item)). (Abgesehen von der Tatsache, dass diese Schleife in der Arbeit nie endet.)
Kotarak
Ja, der erste bricht die Faulheit seiner Argumente. Die resultierende Sequenz hat die gleichen Werte, obwohl es sich nicht mehr um eine faule Sequenz handelt.
Arthur Ulfeldt
+1! Ich habe zahlreiche kleine rekursive Funktionen geschrieben, nur um einen anderen Tag zu finden, an dem diese alle mit mapund / oder verallgemeinert werden können reduce.
nperson325681
5

Sammlungstypen haben für einige Vorgänge unterschiedliche Verhaltensweisen:

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]

Das Arbeiten mit Strings kann verwirrend sein (ich verstehe sie immer noch nicht ganz). Insbesondere sind Zeichenfolgen nicht mit Zeichenfolgen identisch, obwohl Sequenzfunktionen auf sie angewendet werden:

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

Um eine Zeichenfolge wieder herauszuholen, müssen Sie Folgendes tun:

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

zu viele Klammern, insbesondere mit dem Aufruf der Java-Methode void, der zu NPE führt:

public void foo() {}

((.foo))

führt zu NPE aus äußeren Parantheses, da innere Parantheses Null ergeben.

public int bar() { return 5; }

((.bar)) 

Ergebnisse in der einfacher zu debuggen:

java.lang.Integer cannot be cast to clojure.lang.IFn
  [Thrown class java.lang.ClassCastException]
Miaubiz
quelle