Wie man mental Lisp / Clojure-Code liest

76

Vielen Dank für all die schönen Antworten! Kann nicht nur einen als korrekt markieren

Hinweis: Bereits ein Wiki

Ich bin neu in der funktionalen Programmierung und obwohl ich einfache Funktionen in der funktionalen Programmierung lesen kann, z. B. um die Fakultät einer Zahl zu berechnen, fällt es mir schwer, große Funktionen zu lesen. Ein Grund dafür ist, dass ich nicht in der Lage bin, die kleineren Codeblöcke innerhalb einer Funktionsdefinition herauszufinden, und zum Teil, weil es für mich immer schwieriger wird, ( )im Code übereinzustimmen.

Es wäre großartig, wenn mich jemand durch das Lesen eines Codes führen und mir einige Tipps geben könnte, wie man einen Code schnell entschlüsselt.

Hinweis: Ich kann diesen Code verstehen, wenn ich ihn 10 Minuten lang anstarre, aber ich bezweifle, dass derselbe Code, der in Java geschrieben wurde, 10 Minuten dauern würde. Ich denke, um mich im Lisp-Code wohl zu fühlen, muss ich es schneller machen

Hinweis: Ich weiß, dass dies eine subjektive Frage ist. Und ich suche hier keine nachweislich richtige Antwort. Nur Kommentare dazu, wie Sie diesen Code lesen, wären willkommen und sehr hilfreich

(defn concat
  ([] (lazy-seq nil))
  ([x] (lazy-seq x))
  ([x y]
    (lazy-seq
      (let [s (seq x)]
        (if s
          (if (chunked-seq? s)
            (chunk-cons (chunk-first s) (concat (chunk-rest s) y))
            (cons (first s) (concat (rest s) y)))
          y))))
  ([x y & zs]
     (let [cat (fn cat [xys zs]
                 (lazy-seq
                   (let [xys (seq xys)]
                     (if xys
                       (if (chunked-seq? xys)
                         (chunk-cons (chunk-first xys)
                                     (cat (chunk-rest xys) zs))
                         (cons (first xys) (cat (rest xys) zs)))
                       (when zs
                         (cat (first zs) (next zs)))))))]
       (cat (concat x y) zs))))
Benutzer855
quelle
3
Erfahrung? Wenn Sie sich daran gewöhnt haben, Lisp-Code zu lesen, ist er schneller. Eine der Hauptbeschwerden über Lisp ist jedoch, dass es schwer zu lesen ist. Erwarten Sie also nicht, dass es für Sie schnell intuitiv wird.
Nate CK
10
Dies ist keine einfache Funktion. Wenn Sie es nach 10 Minuten vollständig verstehen können, geht es Ihnen gut.
Michiel de Mare

Antworten:

49

Insbesondere Lisp-Code ist aufgrund der regulären Syntax noch schwerer zu lesen als andere funktionale Sprachen. Wojciech gibt eine gute Antwort, um Ihr semantisches Verständnis zu verbessern. Hier finden Sie eine Hilfe zur Syntax.

Machen Sie sich beim Lesen von Code keine Gedanken über Klammern. Sorgen Sie sich um Einrückungen. Die allgemeine Regel lautet, dass Dinge auf derselben Einrückungsstufe miteinander verbunden sind. Damit:

      (if (chunked-seq? s)
        (chunk-cons (chunk-first s) (concat (chunk-rest s) y))
        (cons (first s) (concat (rest s) y)))

Zweitens, wenn Sie nicht alles in eine Zeile einfügen können, rücken Sie die nächste Zeile ein wenig ein. Dies sind fast immer zwei Leerzeichen:

(defn concat
  ([] (lazy-seq nil))  ; these two fit
  ([x] (lazy-seq x))   ; so no wrapping
  ([x y]               ; but here
    (lazy-seq          ; (lazy-seq indents two spaces
      (let [s (seq x)] ; as does (let [s (seq x)]

Drittens, wenn mehrere Argumente für eine Funktion nicht in eine einzelne Zeile passen, richten Sie das zweite, dritte usw. Argument unter der Anfangsklammer des ersten aus. Viele Makros haben eine ähnliche Regel mit Variationen, damit die wichtigen Teile zuerst angezeigt werden.

; fits on one line
(chunk-cons (chunk-first s) (concat (chunk-rest s) y))

; has to wrap: line up (cat ...) underneath first ( of (chunk-first xys)
                     (chunk-cons (chunk-first xys)
                                 (cat (chunk-rest xys) zs))

; if you write a C-for macro, put the first three arguments on one line
; then the rest indented two spaces
(c-for (i 0) (< i 100) (add1 i)
  (side-effects!)
  (side-effects!)
  (get-your (side-effects!) here))

Diese Regeln helfen Ihnen, Blöcke im Code zu finden: wenn Sie sehen

(chunk-cons (chunk-first s)

Zählen Sie keine Klammern! Überprüfen Sie die nächste Zeile:

(chunk-cons (chunk-first s)
            (concat (chunk-rest s) y))

Sie wissen, dass die erste Zeile kein vollständiger Ausdruck ist, da die nächste Zeile darunter eingerückt ist.

Wenn Sie das defn concatvon oben sehen, wissen Sie, dass Sie drei Blöcke haben, weil es drei Dinge auf derselben Ebene gibt. Aber alles unterhalb der dritten Zeile wird darunter eingerückt, sodass der Rest zu diesem dritten Block gehört.

Hier ist ein Styleguide für Scheme . Ich kenne Clojure nicht, aber die meisten Regeln sollten gleich sein, da keiner der anderen Lisps sehr unterschiedlich ist.

Nathan Shively-Sanders
quelle
Wie steuere ich in Ihrem zweiten Codebeispiel den Einzug der fünften Zeile, dh (Lazy-Seq, in Emacs? Standardmäßig ist er in der vorherigen Zeile mit 'y' ausgerichtet.
Wei Hu
Entschuldigung, ich weiß es nicht. Ich verwende keine Clojure und dieses Beispiel lässt sich nicht genau in Schema übersetzen. Sie können nach einer Variablen wie clojure-indent-offset suchen. Zum Beispiel musste ich für Haskell '(haskell-indent-offset 2) zu meinen benutzerdefinierten Variablen hinzufügen.
Nathan Shively-Sanders
59

Ich denke, es concatist ein schlechtes Beispiel, um zu versuchen, es zu verstehen. Es ist eine Kernfunktion und niedriger als Code, den Sie normalerweise selbst schreiben würden, da es effizient sein soll.

Beachten Sie auch, dass Clojure-Code im Vergleich zu Java-Code extrem dicht ist. Ein kleiner Clojure-Code macht viel Arbeit. Der gleiche Code in Java würde nicht 23 Zeilen sein. Es wären wahrscheinlich mehrere Klassen und Schnittstellen, sehr viele Methoden, viele lokale temporäre Wegwerfvariablen und umständliche Schleifenkonstrukte und im Allgemeinen alle Arten von Boilerplates.

Einige allgemeine Tipps ...

  1. Versuchen Sie, die Eltern die meiste Zeit zu ignorieren. Verwenden Sie stattdessen die Einrückung (wie Nathan Sanders vorschlägt). z.B

    (if s
      (if (chunked-seq? s)
        (chunk-cons (chunk-first s) (concat (chunk-rest s) y))
        (cons (first s) (concat (rest s) y)))
      y))))
    

    Wenn ich mir das ansehe, sieht mein Gehirn:

    if foo
      then if bar
        then baz
        else quux
      else blarf
    
  2. Wenn Sie den Cursor auf einen Paren setzen und Ihr Texteditor den passenden nicht syntaktisch hervorhebt, empfehle ich Ihnen, einen neuen Editor zu finden.

  3. Manchmal hilft es, Code von innen nach außen zu lesen. Clojure-Code ist in der Regel tief verschachtelt.

    (let [xs (range 10)]
      (reverse (map #(/ % 17) (filter (complement even?) xs))))
    

    Schlecht: "Also fangen wir mit Zahlen von 1 bis 10 an. Dann kehren wir die Reihenfolge der Zuordnung der Filterung des Komplements der Wartezeit um. Ich habe vergessen, wovon ich spreche."

    Gut: "OK, also nehmen wir etwas xs. (complement even?)Bedeutet das Gegenteil von gerade, also" ungerade ". Wir filtern also eine Sammlung, sodass nur noch die ungeraden Zahlen übrig bleiben. Dann teilen wir sie alle durch 17. Dann wir Ich kehre die Reihenfolge um. Und die xsfraglichen sind 1 bis 10, gotcha. "

    Manchmal hilft es, dies explizit zu tun. Nehmen Sie die Zwischenergebnisse, werfen Sie sie in ein letund geben Sie ihnen einen Namen, damit Sie verstehen. Die REPL ist dafür gemacht, so herumzuspielen. Führen Sie die Zwischenergebnisse aus und sehen Sie, was Ihnen jeder Schritt bringt.

    (let [xs (range 10)
          odd? (complement even?)
          odd-xs (filter odd? xs)
          odd-xs-over-17 (map #(/ % 17) odd-xs)
          reversed-xs (reverse odd-xs-over-17)]
      reversed-xs)
    

    Bald werden Sie in der Lage sein, solche Dinge mental ohne Anstrengung zu tun.

  4. Machen Sie liberalen Gebrauch von (doc). Der Nutzen der Verfügbarkeit von Dokumentation direkt bei der REPL kann nicht genug betont werden. Wenn Sie clojure.contrib.repl-utilsIhre .clj-Dateien verwenden und im Klassenpfad haben, können Sie den (source some-function)gesamten Quellcode dafür anzeigen . Sie können (show some-java-class)eine Beschreibung aller darin enthaltenen Methoden anzeigen. Und so weiter.

Etwas schnell lesen zu können, bringt nur Erfahrung mit sich. Lisp ist nicht schwerer zu lesen als jede andere Sprache. Es kommt einfach so vor, dass die meisten Sprachen wie C aussehen und die meisten Programmierer die meiste Zeit damit verbringen, dies zu lesen. Es scheint also, dass die C-Syntax einfacher zu lesen ist. Üben üben üben.

Brian Carper
quelle
3
+1 für die Erwähnung der 'Quell'-Funktion ist es schwieriger, an diese Informationen zu kommen, als sie sein sollten. Gehen Sie nun den Rest von clojure.contrib.repl-utils durch, um zu sehen, was ich vermisst habe.
Vedang
3
+1 für das Werfen von Zwischenergebnissen in einem Let, machte den Code so viel lesbarer (für einen Clojure-Neuling)
Greg K
Jemand sollte wirklich einige Artikel oder sogar ein Buch darüber schreiben, wie man LISP als Nicht-LISP-Programmierer lernt (z. B. aus Python). Es scheint so anders zu sein, dass wir Nicht-LISPer lernen müssen, wie man LISP lernt. Nach dieser Erklärung scheint es nicht ganz so undurchdringlich zu sein. Vorher habe ich mir den LISP-Code einmal angesehen und mir gedacht: "Was bringt es, es zu versuchen?".
Bob
Kennt jemand solche Bücher, die er vorschlagen könnte?
Bob
7

Denken Sie zunächst daran, dass das Funktionsprogramm aus Ausdrücken und nicht aus Anweisungen besteht. Zum Beispiel nimmt form (if condition expr1 expr2) sein erstes Argument als Bedingung, um den booleschen Fehler zu testen, wertet ihn aus, und wenn er auf true ausgewertet wird, wertet er expr1 aus und gibt ihn zurück, andernfalls wertet er expr2 aus und gibt ihn zurück. Wenn jedes Formular einen Ausdruck zurückgibt, verschwinden möglicherweise einige der üblichen Syntaxkonstrukte wie THEN- oder ELSE-Schlüsselwörter. Beachten Sie, dass hier ifselbst auch ein Ausdruck ausgewertet wird.

Nun zur Auswertung: In Clojure (und anderen Lisps) sind die meisten Formulare Funktionsaufrufe des Formulars (f a1 a2 ...), bei denen alle Argumente fvor dem eigentlichen Funktionsaufruf ausgewertet werden. Formulare können aber auch Makros oder spezielle Formulare sein, die einige (oder alle) ihrer Argumente nicht bewerten. Im Zweifelsfall konsultieren Sie die Dokumentation (doc f)oder checken Sie einfach REPL ein:

user=> apply
#<core$apply__3243 clojure.core$apply__3243@19bb5c09>
eine Funktion ein Makro.
user=> doseq
java.lang.Exception: Can't take value of a macro: #'clojure.core/doseq

Diese beiden Regeln:

  • Wir haben Ausdrücke, keine Aussagen
  • Die Auswertung eines Unterformulars kann erfolgen oder nicht, je nachdem, wie sich das äußere Formular verhält

sollte das Durchsuchen von Lisp-Programmen erleichtern, insb. wenn sie schöne Einrückungen haben, wie das Beispiel, das Sie gegeben haben.

Hoffe das hilft.

Wojciech Kaczmarek
quelle