Clojure: Ruhe gegen Weiter

70

Es fällt mir schwer, den Unterschied zwischen restund nextin Clojure zu verstehen . Die Seite der offiziellen Website über Faulheit zeigt an, dass die Präferenz wahrscheinlich die Verwendung sein sollte rest, aber sie erklärt den Unterschied zwischen den beiden nicht wirklich klar. Kann jemand einen Einblick geben?

Daniel Yankowsky
quelle
4
Dieser Punkt ist keine gesonderte Antwort wert: Was die Antworten bisher nicht vollständig explizit gemacht haben, ist, dass dies ()wahr und nilfalsch ist. Also (defn keep-going [my-coll] (if (rest my-coll) (keep-going (rest my-coll)) "Finished.")wird es für immer weitergehen - oder besser gesagt, der Stapel wird überlaufen - während (defn keep-going [my-coll] (if (next my-coll) (keep-going (next my-coll)) "Finished.")es fertig ist. (OP hat dies sicherlich inzwischen herausgefunden; ich füge diese Bemerkung für andere hinzu.)
Mars

Antworten:

62

Wie die von Ihnen verlinkte Seite beschrieben hat, nextist sie strenger als (das neue Verhalten von), restda sie die Struktur der faulen Nachteile bewerten muss, um zu wissen, ob sie zurückkehren soll niloder eine Folge.

restAuf der anderen Seite wird immer eine Sequenz zurückgegeben, sodass nichts ausgewertet werden muss, bis Sie das Ergebnis von tatsächlich verwenden rest. Mit anderen Worten, restist fauler als next.

sepp2k
quelle
OK, ich glaube ich verstehe vielleicht was du meinst. Wollen Sie damit sagen, dass nextimmer eine Nachteile-Zelle zurückgegeben wird, während restlediglich ein ISeq (oder welcher Typ auch immer) zurückgegeben wird?
Daniel Yankowsky
1
@ Daniel: Gibt nextzurück niloder ist nicht leer seq. restGibt immer ein zurück seq, das möglicherweise leer ist.
sepp2k
Ich gehe davon aus, dass es für einige Sequenzen eine teure Operation ist, festzustellen, ob mehr Elemente vorhanden sind. Bei einer normalen Liste von Nachteile-Zellen würde ich denken, dass es trivial ist, festzustellen, ob Sie das Ende der Liste erreicht haben. Bei einer Lazy-Sequenz (vielleicht von Lazy-Seq) kann diese Bestimmung jedoch teuer sein. Ich habe nicht ganz verstanden, dass Null anders ist als die leere Sequenz. Es hört sich so an, als würden Teilsequenzen, die mit Ruhe erzeugt werden, schwächere Garantien abgeben und können daher effizienter sein.
Daniel Yankowsky
5
Zur Verwirrung hinzufügen: (drop 1 s)ist fauler als restweil es keine teilweise Verwirklichung von s erzwingt . restist nicht garantiert, nur den 1. Punkt der Lazy Seq zu realisieren. Es kommt auf die faule Sequenz an.
Cgrand
@DavidTonhofer Mit Clojure 1.10.1 gibt dieser Code "make-empty was called" aus, dieser Code jedoch nicht. Es sieht für mich so aus, als ob der Unterschied immer noch da ist.
sepp2k
33

Es ist einfach, wenn Sie dies haben:

(next '(1))
=> nil

So nextschaut auf die nächste Sache , und wenn die Zeile leer ist es kehrt nilanstelle eines leeren fg. Dies bedeutet, dass es nach vorne schauen muss (bis zum ersten Element, das es zurückgeben würde), was es nicht vollständig faul macht (möglicherweise benötigen Sie nicht den nächsten Wert, nextverschwenden aber die Rechenzeit, um nach vorne zu schauen).

(rest '(1))
=> ()

rest schaut nicht nach vorne und gibt nur den Rest der Sequenz zurück.

Vielleicht denkst du, warum überhaupt zwei verschiedene Dinge hier verwenden? Der Grund dafür ist, dass Sie normalerweise wissen möchten, ob in der Sequenz nichts mehr vorhanden ist, und einfach zurückkehren möchten. nilIn einigen Fällen, in denen die Leistung sehr wichtig ist und die Bewertung eines weiteren Elements einen enormen Aufwand bedeuten kann, können Sie diesen verwenden rest.

Nickik
quelle
7
Nur um es zu Tode zu schlagen: (Rest Null) => ()
Gtrak
22

nextist wie (seq (rest ...)).

restgibt das verbleibende Stück einer Sequenz zurück. Wenn dieses Stück der Sequenz noch nicht realisiert wurde, wird restes nicht erzwungen. Es wird Ihnen nicht einmal sagen, ob noch weitere Elemente in der Sequenz vorhanden sind.

nextmacht dasselbe, erzwingt dann aber, dass mindestens ein Element der Sequenz realisiert wird. Wenn Sie also nextzurückkehren nil, wissen Sie, dass die Sequenz keine weiteren Elemente mehr enthält.

Stuart Sierra
quelle
3

Ich bevorzuge jetzt die Verwendung nextmit Rekursion, da die Escape-Bewertung einfacher / sauberer ist:

(loop [lst a-list]
    (when lst
        (recur (next lst))

vs.

(loop [lst a-list]
    (when-not (empty? lst)   ;; or (when (seq? lst)
        (recur (rest lst))

Ein Fall für die Verwendung restwäre jedoch, wenn Sie eine Sammlung als Warteschlange oder Stapel verwenden. In diesem Fall möchten Sie, dass Ihre Funktion eine leere Sammlung zurückgibt, wenn Sie das letzte Element entfernen oder aus der Warteschlange entfernen.

Terje Dahl
quelle
Das erste Beispiel führt einen nutzlosen rekursiven Aufruf durch, wenn er lstleer ist, weil die leere Liste wahr ist. Ein guter Weg, um auf "Nicht-Null" zu testen: (when (some? lst) ...(über Tonsky.me/blog/readable-clojure )
David Tonhofer
Ich benutze normalerweise if-let, wie in: (loop [lst lst] (if-let [[elem & lst] (seq lst)] (recur lst)))
Daniel Yankowsky
1

Hier ist eine kleine Tabelle, die nützlich ist, wenn Sie Code schreiben, der eine Sequenz mithilfe der "profanen" Rekursion (mithilfe des Stapels) oder mithilfe recur(mithilfe der schwanzrekursiven Optimierung, also der eigentlichen Schleife) durchläuft .

Ruhe, als nächstes zuerst, seq, oh mein Gott!

Beachten Sie die Unterschiede im Verhalten von restund next. In Kombination seqdamit ergibt sich die folgende Redewendung, bei der das Ende der Liste über seqund der Rest der Liste über rest(angepasst aus "The Joy of Clojure") getestet wird :

; "when (seq s)":
; case s nonempty -> truthy -> go
; case s empty    -> nil -> falsy -> skip
; case s nil      -> nil -> falsy -> skip

(defn print-seq [s]
  (when (seq s)          
     (assert (and (not (nil? s)) (empty? s)))
     (prn (first s))     ; would give nil on empty seq
     (recur (rest s))))  ; would give an empty sequence on empty seq

Warum ist nexteifriger als rest?

Wenn (next coll)ausgewertet wird, kann das Ergebnis sein nil. Dies muss sofort bekannt sein (dh nilmuss tatsächlich zurückgegeben werden), da der Anrufer basierend auf der Wahrheit von verzweigen kann nil.

Wenn (rest coll)ausgewertet wird, kann das Ergebnis nicht sein nil. Wenn der Aufrufer das Ergebnis dann nicht mithilfe eines Funktionsaufrufs auf Leere testet, kann die Erzeugung eines "nächsten Elements" in einer Lazy-Seq auf die tatsächlich benötigte Zeit verzögert werden.

Beispiel

Eine völlig faule Sammlung, alle Berechnungen werden "bis zur Verwendung angehalten".

(def x
   (lazy-seq
      (println "first lazy-seq evaluated")
      (cons 1
         (lazy-seq
            (println "second lazy-seq evaluated")
            (cons 2
               (lazy-seq
                  (println "third lazy-seq evaluated")))))))           

;=> #'user/x

Die Berechnung von "x" wird nun beim ersten "Lazy-Seq" ausgesetzt.

Mit dem eifrigen nächsten sehen wir zwei Bewertungen:

(def y (next x))

;=> first lazy-seq evaluated
;=> second lazy-seq evaluated
;=> #'user/y

(type y)

;=> clojure.lang.Cons

(first y)

;=> 2
  • Die erste Lazy-Seq wird ausgewertet, was zum Ausdruck führt first lazy-seq evaluated
  • Dies führt zu einer nicht leeren Struktur: einem Nachteil mit 1 links und einem Lazy-Seq rechts.
  • nextmuss möglicherweise zurückkehren, nilwenn der rechte Zweig leer ist. Wir müssen also eine Ebene tiefer prüfen.
  • Die zweite Lazy-Seq wird ausgewertet, was zum Ausdruck führt second lazy-seq evaluated
  • Dies führt zu einer nicht leeren Struktur: einem Nachteil mit 2 links und einem Lazy-Seq rechts.
  • Also nicht zurückkehren nil, sondern die Nachteile zurückgeben.
  • Wenn Sie das firstvon erhalten y, gibt es nichts zu tun, außer 2 von den bereits erhaltenen Nachteilen abzurufen.

Bei Verwendung des Lazer sehen restwir eine Bewertung (beachten Sie, dass Sie xzuerst neu definieren müssen , damit dies funktioniert).

(def y (rest x))

;=> first lazy-seq evaluated
;=> #'user/y

(type y)

;=> clojure.lang.LazySeq

(first y)

;=> second lazy-seq evaluated
;=> 2
  • Die erste Lazy-Seq wird ausgewertet, was zum Ausdruck führt first lazy-seq evaluated
  • Dies führt zu einer nicht leeren Struktur: einem Nachteil mit 1 links und einem Lazy-Seq rechts.
  • restkehrt niemals zurück nil, selbst wenn die Lazy-Seq auf der rechten Seite die leere Seq ergibt.
  • Wenn der Anrufer mehr wissen muss (ist die Sequenz leer?), Kann er später den entsprechenden Test für die Lazy-Sequenz durchführen.
  • Also sind wir fertig, geben Sie einfach die Lazy-Seq als Ergebnis zurück.
  • Beim Erhalten des firstvon ymuss das Lazy-Seq einen Schritt weiter ausgewertet werden, um das 2 zu erhalten

Seitenleiste

Beachten Sie, dass yder Typ ist LazySeq. Dies mag offensichtlich erscheinen, ist aber LazySeqnicht "Sache der Sprache", sondern "Sache der Laufzeit", die kein Ergebnis, sondern einen Berechnungszustand darstellt. In der Tat (type y)ist clojure.lang.LazySeqbedeutet nur , „wir wissen nicht , den Typ noch, Sie mehr zu erfahren zu tun haben“. Immer wenn eine Clojure-Funktion wie nil?etwas trifft, das einen Typ hat clojure.lang.LazySeq, wird eine Berechnung durchgeführt!

PS

In Joy of Clojure, 2. Auflage, auf Seite 126, wird anhand eines Beispiels iterateder Unterschied zwischen nextund veranschaulicht rest.

(doc iterate)
;=> Returns a lazy sequence of x, (f x), (f (f x)) etc. f must be free
;   of side-effects

Wie sich herausstellt, funktioniert das Beispiel nicht. In diesem Fall gibt es tatsächlich keinen Unterschied im Verhalten zwischen nextund rest. Ich nextweiß nicht warum, weiß vielleicht, dass es niemals nilhierher zurückkehren wird und verwendet standardmäßig das Verhalten von rest.

(defn print-then-inc [x] (do (print "[" x "]") (inc x)))

(def very-lazy (iterate print-then-inc 1))

(def very-lazy (rest(rest(rest(iterate print-then-inc 1)))))
;=> [ 1 ][ 2 ]#'user/very-lazy
(first very-lazy)
;=> [ 3 ]4

(def less-lazy (next(next(next(iterate print-then-inc 1)))))
;=> [ 1 ][ 2 ]#'user/less-lazy
(first less-lazy)
;=> [ 3 ]4
David Tonhofer
quelle
Der Teil "Faulheit" wurde behoben, weil sepp2k mir sagte, dass ich völlig falsch liege.
David Tonhofer