2009-12-12 10 views
70

Grazie mille per tutte le bellissime risposte! Non può segnare solo uno come correttaCome leggere mentalmente codice Lisp/Clojure

Nota: Already a wiki

Sono nuovo di programmazione funzionale e mentre posso leggere semplici funzioni di programmazione funzionale, per esempio calcolando il fattoriale di un numero, trovo difficile leggere grandi funzioni. Parte del motivo è che penso a causa della mia incapacità di capire i blocchi di codice più piccoli all'interno di una definizione di funzione e anche in parte perché mi risulta difficile per me corrispondere a () nel codice.

Sarebbe bello se qualcuno potesse guidarmi leggendo un codice e darmi alcuni suggerimenti su come decifrare rapidamente un codice.

Nota: riesco a capire questo codice se lo fisso per 10 minuti, ma dubito che se questo stesso codice fosse stato scritto in Java, ci sarebbero voluti 10 minuti. Quindi, penso di sentirmi a mio agio nel codice stile Lisp, devo farlo più velocemente

Nota: so che questa è una domanda soggettiva. E non sto cercando alcuna risposta provabilmente corretta qui. Proprio commenti su come si fa a leggere questo codice, sarebbe il benvenuto e altamente disponibile

(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)))) 
+3

Esperienza? Se ti abitui a leggere il codice Lisp, sarà più veloce. Tuttavia, una delle principali lamentele su Lisp è che è difficile da leggere, quindi non aspettarti che diventi intuitivo per te rapidamente. –

+10

Questa non è una funzione facile. Se riesci a capirlo pienamente dopo 10 minuti, stai bene. –

risposta

47

Il codice Lisp, in particolare, è ancora più difficile da leggere rispetto ad altri linguaggi funzionali a causa della sintassi regolare. Wojciech offre una buona risposta per migliorare la comprensione semantica. Ecco qualche aiuto sulla sintassi.

Innanzitutto, durante la lettura del codice, non preoccuparti delle parentesi. Preoccupati per l'indentazione. La regola generale è che le cose allo stesso livello di rientro sono correlate. Quindi:

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

In secondo luogo, se non è possibile inserire tutto su una riga, indentare la riga successiva una piccola quantità. Questo è quasi sempre due spazi:

(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)] 

In terzo luogo, se più argomenti a una funzione non possono stare su una singola linea, allineare il secondo, terzo, ecc argomenti sotto della prima partenza parentesi. Molte macro hanno una regola simile con le varianti per consentire la visualizzazione delle parti importanti.

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

Queste regole aiutarti a trovare i blocchi all'interno del codice: se vedete

(chunk-cons (chunk-first s) 

Non contare parentesi! Controllare la riga successiva:

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

Voi sapete che la prima linea non è un'espressione completa perché la linea successiva è rientrato sotto di esso.

Se vedi lo defn concat dall'alto, sai di avere tre blocchi, perché ci sono tre cose sullo stesso livello. Ma tutto sotto la terza linea è rientrato al di sotto di esso, quindi il resto appartiene a quel terzo blocco.

Here is a style guide for Scheme. Non conosco Clojure, ma la maggior parte delle regole dovrebbe essere la stessa poiché nessuno degli altri Lisps varia molto.

+0

Nel tuo secondo esempio di codice, come controllo il rientro della quinta riga, ovvero (lazy-seq, in Emacs? Per impostazione predefinita, è allineato con 'y' sulla riga precedente. –

+0

Scusa, non lo so Non utilizzo clojure e quell'esempio non si traduce precisamente in Scheme. Puoi controllare una variabile come clojure-indent-offset.Ad esempio, per Haskell, dovevo aggiungere '(haskell-indent-offset 2) alle mie variabili personalizzate. –

+0

Ecco una guida di stile per Clojure https://github.com/bbatsov/clojure-style-guide –

7

Prima ricordare che programma funzionale è costituito da espressioni, non dichiarazioni. Ad esempio, il modulo (if condition expr1 expr2) prende il suo primo argomento come condizione per testare il falue booleano, lo valuta e, se viene valutato a true, valuta e restituisce expr1, altrimenti valuta e restituisce expr2. Quando ogni form restituisce un'espressione, alcuni dei soliti costrutti di sintassi come THEN o ELSE possono semplicemente scomparire. Notare che qui lo stesso if valuta anche un'espressione.

Ora sulla valutazione: In Clojure (e altri Lisps) la maggior parte delle forme che si incontrano sono chiamate di funzione del modulo (f a1 a2 ...), dove tutti gli argomenti a f vengono valutati prima della chiamata di funzione effettiva; ma le forme possono essere anche macro o forme speciali che non valutano alcuni (o tutti) i suoi argomenti. In caso di dubbio, consultare la documentazione (doc f) o solo il check-in REPL:

user=> apply
#<core$apply__3243 [email protected]>
una funzione
user=> doseq
java.lang.Exception: Can't take value of a macro: #'clojure.core/doseq
una macro.

Queste due regole:

  • abbiamo espressioni, non dichiarazioni
  • valutazione di una sottomaschera può verificarsi o meno, a seconda di come esterno forma si comporta

dovrebbe facilitare il vostro groking di Lisp programmi, esp. se hanno una buona indentazione come l'esempio che hai dato.

Spero che questo aiuti.

58

Penso che concat sia un cattivo esempio per cercare di capire. È una funzione di base ed è più a basso livello del codice che normalmente si scrive, perché si sforza di essere efficiente.

Un'altra cosa da tenere a mente è che il codice Clojure è estremamente denso rispetto al codice Java. Un piccolo codice Clojure fa molto lavoro. Lo stesso codice in Java non sarebbe 23 righe. Sarebbe probabilmente più classi e interfacce, un gran numero di metodi, un sacco di variabili temporanee di lancio temporanee e costrutti di loop awkward e generalmente tutti i tipi di piastre.

Alcuni consigli generali, però ...

  1. cercate di ignorare le parentesi maggior parte del tempo. Usa invece il rientro (come suggerisce Nathan Sanders). per esempio.

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

    Quando guardo che il mio cervello vede:

    if foo 
        then if bar 
        then baz 
        else quux 
        else blarf 
    
  2. Se si posiziona il cursore su una parentesi e il vostro editor di testo non sintassi evidenziare la corrispondenza uno, vi suggerisco di trovare un nuovo editor.

  3. A volte può essere utile leggere il codice all'interno. Il codice Clojure tende ad essere profondamente annidato.

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

    Bad: "Così si comincia con i numeri da 1 a 10. Poi stiamo invertendo l'ordine della mappatura del filtraggio del complemento dell'attesa ho dimenticato di cosa sto parlando."

    Buono:.. ". OK, quindi stiamo prendendo un po 'di xs(complement even?) significa l'opposto di anche, in modo 'strano' Quindi stiamo filtrando qualche raccolta in modo che solo i numeri dispari sono lasciati Allora siamo dividendoli tutti per 17. Quindi stiamo invertendo l'ordine di loro e gli xs in questione sono da 1 a 10, gotcha. "

    A volte è utile farlo esplicitamente. Prendi i risultati intermedi, gettali in un let e dai loro un nome così capisci. Il REPL è fatto per giocare in questo modo. Esegui i risultati intermedi e scopri cosa ti dà ogni passaggio.

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

    Presto sarete in grado di fare questo genere di cose mentalmente senza sforzo.

  4. Fare un uso liberale di (doc). L'utilità di avere documentazione disponibile direttamente al REPL non può essere sopravvalutata. Se si utilizza clojure.contrib.repl-utils e si hanno i file .clj sul classpath, è possibile eseguire (source some-function) e vedere tutto il codice sorgente per esso. Puoi fare (show some-java-class) e vedere una descrizione di tutti i metodi in esso contenuti. E così via.

Essere in grado di leggere qualcosa in modo rapido viene solo con l'esperienza. Lisp non è più difficile da leggere rispetto a qualsiasi altra lingua. Si dà il caso che la maggior parte delle lingue assomigli a C, e la maggior parte dei programmatori passa la maggior parte del tempo a leggerlo, quindi sembra che la sintassi C sia più facile da leggere. Pratica pratica pratica.

+3

+1 per menzionare la funzione 'fonte', quel bit di informazione è più difficile da trovare per il resto di clojure.contrib.repl-utils per vedere quello che ho perso. – vedang

+3

+1 per il lancio di risultati intermedi in un let, ha reso il codice molto più leggibile (per un novizio Clojure) –