Voll vom Lazy-Bug gebissen

Oder: Warum funktioniert mein for-loop nicht?

Das ist jetzt weniger eine Frage sondern eher eine Erklärung was mich da die letzte Zeit ziemlich genervt hat.

Angenommen ich habe eine Schleife, die Zahlen bis 10 ausgibt.
[clojure](for [x (range 10)]
(println x))
[/clojure]

Dann funktioniert das soweit erstmal in der Repl. Die Zahlen werden ausgegeben und die Rückgabe ist eine Liste mit 5x nil.

Packt man das ganze in eine Funktion
[clojure](defn f []
(for [x (range 5)]
(println x)))

(f)[/clojure]

Und ruft diese auf, so ist weiterhin alles gut und man erhält das erwartete Verhalten.

Ändert man nun die Funktion, so daß nach der Schleife der String “Done!” zurückgegeben wird
[clojure](defn f []
(for [x (range 5)]
(println x))
“Done!”)

(f)[/clojure]

dann beißt einen der lazy-Bug. Die Schleife wird übersprungen und es wird lediglich “Done!” zurückgegeben.

Ein Punkt an dem man fast wahnsinnig werden kann.

Wie kommt es dazu?
Das for ist keine Schleife im Sinne von Java, sondern ein Macro mit einem Rückgabetyp.
Der Rückgabetyp ist eine lazy-seq. Ähnlich einem Iterator in Java.
Und ein Iterator mach sich nur die Arbeit wenn auch tatsächlich darüber iteriert wird.

Da in Clojure das letzte Element einer Funktion ein Rückgabewert ist, wird es in den ersten Fällen zurückgegeben und in der Repl evaluiert und somit ausgegeben.

Im letzten Fall, wird der Iterator nur in der Methode definiert aber nie aufgerufen, was dazu führt das der Schleifeninhalt wie übersprungen wirkt. Zurückgegeben wird nur der String “Done!”.

Was tun?
Eine Möglichkeit ist eine Funktion auf die lazy-seq anzuwenden. z.B. count um die Elemente zu zählen. Das funktioniert zwar ist aber eher ein Schuß von Hinten durch die Brust ins Auge.

[clojure](defn f []
(count (for [x (range 5)]
(println x)))
“Done!”)[/clojure]

Oder man verwendet das wohl korrekte doseq. Das hat die gleiche Syntax wie for, ist aber nicht so lazy.
Die Doku erwähnt sogar (presumably for side-effects) was die Ausgabe in der Konsole in der Tat ist.

[clojure](defn f []
(doseq [x (range 5)]
(println x))
“Done!”)[/clojure]

Zu beachten ist hier, dass doseq nil zurückliefern würde, wohingegen das for 5x nil zurückliefern würde.

[clojure](defn f []
(doseq [x (range 5)]
(println x)
x))

(defn f []
(for [x (range 5)]
(do
(println x)
x)))[/clojure]

Bzw. doseq nil und for (0, 1, 2, 3, 4)

Hi,

danke für den kleinen Exkurs, auf jeden Fall interessant das Problem so aufgedröselt zu bekommen.
Was ich mich nur frage, gibt es überhaupt Vorteile das Konstrukt “for” bei einer funktionalen Sprache zu benutzen?

Gruß,
Tim

Hi Tim,

Eine Alternative zu “for” ist ein “map”. Map ist übrigens auch lazy und sorgt für die gleiche Problematik.
Das for ist in dem Sinne nicht mit dem for aus Java zu vergleichen.

Das for-Konstrukt finde ich praktisch wenn über mehrere Listen iteriert wird, z.B. Pixel eines Bildes
(for [x (range width) y (range height)] (foo x y)) und oft nutze ich das ganze auch um Variablen direkt zu definieren, anstatt hier mit zusätzlichen let’s zu hantieren.

[clojure](for [x datastructure, y (first x), z(second x)]
(foo y z))[/clojure]

Mit map sieht das iterieren über mehrere Listen immer etwas ungemütlich aus. Wobei es hier auch bestimmt coole Varianten gibt.

Das Problem das ich mit dem lazy hatte war, dass ich eigentlich nichts funktionales, sondern etwas prozedurales gebraucht habe. Etwas mit Seiteneffekten. Da kommt man nicht immer herum. Für den Rest des Projekt habe ich super Clojure DSL’s gefunden, aber am Gluecode hat es dann etwas gehakt. Es ist halt ein komisches Gefühl wenn etwas in der Repl Resultate liefert, aber dann in einer main-Methode völlig übersprungen wird.

[QUOTE=Unregistriert][…]
dann beißt einen der lazy-Bug. Die Schleife wird übersprungen und es wird lediglich “Done!” zurückgegeben.
[…]
[/QUOTE]

Es ist eben so das die for-Form kein “for-loop” ist, ich verstehe aber das man es verwechseln könnte. Das ganze nennt sich “List Comprehension” und die Ergebnisliste ist in Clojure nun leider eine LazySeq. Dabei wird die for-Form nicht übersprungen nur wird die resultierende LazySeq nirgendwo verwendet. Deshalb wie du schon in Erfahrung gebracht hast, entweder doseq, was mehr dem imperativen “for-each” entspricht oder alternativ (wobei man das seltener sieht) man wrappt die for-Form in einer doall-Form.