Per favore, spiega alcuni dei punti di Paul Graham su Lisp


146

Ho bisogno di aiuto per comprendere alcuni dei punti di What Grap Different di Paul Graham .

  1. Un nuovo concetto di variabili. In Lisp, tutte le variabili sono effettivamente puntatori. I valori sono quelli che hanno tipi, non variabili, e assegnare o associare variabili significa copiare i puntatori, non ciò a cui puntano.

  2. Un tipo di simbolo. I simboli differiscono dalle stringhe in quanto è possibile verificare l'uguaglianza confrontando un puntatore.

  3. Una notazione per il codice che usa alberi di simboli.

  4. L'intera lingua è sempre disponibile. Non esiste una vera distinzione tra tempo di lettura, tempo di compilazione e runtime. È possibile compilare o eseguire il codice durante la lettura, leggere o eseguire il codice durante la compilazione e leggere o compilare il codice in fase di esecuzione.

Cosa significano questi punti? In che modo differiscono in lingue come C o Java? Ci sono altre lingue diverse dalle lingue della famiglia Lisp con uno di questi costrutti ora?


10
Non sono sicuro che il tag di programmazione funzionale sia garantito qui, poiché è ugualmente possibile scrivere codice imperativo o OO in molti Lisps in quanto è scrivere codice funzionale - e in effetti c'è un sacco di Lisp non funzionale codice in giro. Suggerirei di rimuovere il tag fp e aggiungere invece clojure - si spera che ciò possa portare alcuni input interessanti dai Lisper basati su JVM.
Michał Marczyk,

58
Abbiamo anche un paul-grahamtag qui? !!! Fantastico ...
missingfaktor,

@missingfaktor Forse ha bisogno di una richiesta di burninate
cat

Risposte:


98

La spiegazione di Matt è perfettamente a posto - e lui prende una possibilità in un confronto con C e Java, cosa che non farò - ma per qualche ragione mi diverto davvero a discutere di questo argomento una volta ogni tanto, quindi - ecco il mio scatto a una risposta.

Sui punti (3) e (4):

I punti (3) e (4) sul tuo elenco sembrano i più interessanti e ancora rilevanti al momento.

Per capirli, è utile avere un quadro chiaro di ciò che accade con il codice Lisp - sotto forma di un flusso di caratteri digitati dal programmatore - sulla sua strada per essere eseguito. Facciamo un esempio concreto:

;; a library import for completeness,
;; we won't concern ourselves with it
(require '[clojure.contrib.string :as str])

;; this is the interesting bit:
(println (str/replace-re #"\d+" "FOO" "a123b4c56"))

Questo frammento di codice Clojure viene stampato aFOObFOOcFOO. Nota che Clojure probabilmente non soddisfa pienamente il quarto punto del tuo elenco, poiché il tempo di lettura non è realmente aperto al codice utente; Discuterò cosa significherebbe per questo essere diversamente, però.

Quindi, supponiamo di avere questo codice in un file da qualche parte e chiediamo a Clojure di eseguirlo. Inoltre, supponiamo (per semplicità) di aver superato l'importazione della libreria. La parte interessante inizia da (printlne termina )all'estrema destra. Questo è lessato / analizzato come ci si aspetterebbe, ma sorge già un punto importante: il risultato non è una rappresentazione AST specifica del compilatore speciale - è solo una normale struttura di dati Clojure / Lisp , vale a dire un elenco nidificato contenente un gruppo di simboli, stringhe e - in questo caso - un singolo oggetto modello regex compilato corrispondente al#"\d+"letterale (più su questo sotto). Alcuni Lisps aggiungono i loro piccoli colpi di scena a questo processo, ma Paul Graham si riferiva principalmente al Common Lisp. Sui punti rilevanti per la tua domanda, Clojure è simile a CL.

L'intera lingua in fase di compilazione:

Dopo questo punto, tutto il compilatore si occupa (questo sarebbe anche vero per un interprete Lisp; il codice Clojure sembra essere sempre compilato) sono strutture di dati Lisp che i programmatori Lisp sono abituati a manipolare. A questo punto diventa evidente una meravigliosa possibilità: perché non consentire ai programmatori Lisp di scrivere funzioni Lisp che manipolano i dati Lisp che rappresentano i programmi Lisp e producono dati trasformati che rappresentano programmi trasformati, da utilizzare al posto degli originali? In altre parole: perché non consentire ai programmatori Lisp di registrare le loro funzioni come plug-in di compilatore, chiamati macro in Lisp? E in effetti qualsiasi sistema Lisp decente ha questa capacità.

Quindi, le macro sono normali funzioni Lisp che operano sulla rappresentazione del programma in fase di compilazione, prima della fase di compilazione finale quando viene emesso il codice oggetto reale. Dal momento che non ci sono limiti ai tipi di macro di codice che possono essere eseguite (in particolare, il codice che eseguono è spesso esso stesso scritto con l'uso liberale della funzione macro), si può dire che "l'intera lingua è disponibile al momento della compilazione ".

L'intera lingua al momento della lettura:

Torniamo a quel #"\d+"regex letterale. Come accennato in precedenza, questo viene trasformato in un vero oggetto modello compilato al momento della lettura, prima che il compilatore ascolti la prima menzione del nuovo codice in preparazione per la compilazione. Come succede?

Bene, il modo in cui Clojure è attualmente implementato, l'immagine è in qualche modo diversa da quella che Paul Graham aveva in mente, anche se tutto è possibile con un trucco intelligente . In Common Lisp, la storia sarebbe leggermente più pulita concettualmente. Le basi sono comunque simili: Lisp Reader è una macchina a stati che, oltre a eseguire transizioni di stato e infine a dichiarare se ha raggiunto uno "stato di accettazione", sputa le strutture di dati Lisp rappresentate dai caratteri. Quindi i caratteri 123diventano il numero 123ecc. Il punto importante viene ora: questa macchina a stati può essere modificata dal codice utente. (Come notato in precedenza, è del tutto vero nel caso di CL; per Clojure, è richiesto un hack (scoraggiato e non usato nella pratica). Ma sto divagando, è l'articolo di PG che dovrei elaborare, quindi ...)

Quindi, se sei un programmatore di Common Lisp e ti piace l'idea dei letterali vettoriali in stile Clojure, puoi semplicemente collegare al lettore una funzione per reagire in modo appropriato a una sequenza di caratteri - [o #[forse - e trattarla come l'inizio di un vettore letterale che termina alla corrispondenza ]. Tale funzione è chiamata macro lettore e, proprio come una macro normale, può eseguire qualsiasi tipo di codice Lisp, incluso il codice che è stato esso stesso scritto con notazione funky abilitata da macro lettore precedentemente registrate. Quindi c'è l'intera lingua al momento della lettura per te.

Avvolgendolo:

In realtà, ciò che è stato dimostrato finora è che si possono eseguire le normali funzioni Lisp al momento della lettura o della compilazione; l'unico passo che uno deve fare da qui per capire come la lettura e la compilazione sono esse stesse possibili in fase di lettura, compilazione o runtime è rendersi conto che la lettura e la compilazione sono esse stesse eseguite dalle funzioni di Lisp. Puoi semplicemente chiamare reado evalin qualsiasi momento leggere i dati Lisp dai flussi di caratteri o compilare ed eseguire il codice Lisp, rispettivamente. Questa è l'intera lingua proprio lì, sempre.

Nota come il fatto che Lisp soddisfi il punto (3) della tua lista è essenziale per il modo in cui riesce a soddisfare il punto (4) - il particolare sapore delle macro fornito da Lisp si basa fortemente sul codice rappresentato da regolari dati Lisp, che è abilitato da (3). Per inciso, solo l'aspetto "albero-ish" del codice è davvero cruciale qui - si potrebbe concepire un Lisp scritto usando XML.


4
Attenzione: dicendo "macro normale (compilatore)", sei vicino a sottintendere che le macro del compilatore sono macro "regolari", quando in Common Lisp (almeno), "macro del compilatore" è una cosa molto specifica e diversa: lispworks. com / documentazione / lw51 / CLHS / Body /…
Ken,

Ken: Buona cattura, grazie! Lo cambierò in "macro normale", che penso sia improbabile che inciampi chiunque.
Michał Marczyk,

Risposta fantastica. Ho imparato di più da esso in 5 minuti di quanto non abbia fatto in ore cercando / riflettendo sulla domanda. Grazie.
Charlie Flowers

Modifica: argh, frainteso una frase run-on. Corretto per la grammatica (è necessario un "peer" per accettare la mia modifica).
Tatiana Racheva,

Le espressioni S e XML possono dettare le stesse strutture, ma XML è molto più dettagliato e quindi non adatto alla sintassi.
Sylwester,

66

1) Un nuovo concetto di variabili. In Lisp, tutte le variabili sono effettivamente puntatori. I valori sono quelli che hanno tipi, non variabili, e assegnare o associare variabili significa copiare i puntatori, non ciò a cui puntano.

(defun print-twice (it)
  (print it)
  (print it))

'it' è una variabile. Può essere associato a QUALSIASI valore. Non ci sono restrizioni e nessun tipo associato alla variabile. Se si chiama la funzione, non è necessario copiare l'argomento. La variabile è simile a un puntatore. Ha un modo per accedere al valore associato alla variabile. Non è necessario riservare memoria. Siamo in grado di passare qualsiasi oggetto dati quando chiamiamo la funzione: qualsiasi dimensione e qualsiasi tipo.

Gli oggetti dati hanno un 'tipo' e tutti gli oggetti dati possono essere interrogati per il suo 'tipo'.

(type-of "abc")  -> STRING

2) Un tipo di simbolo. I simboli differiscono dalle stringhe in quanto è possibile verificare l'uguaglianza confrontando un puntatore.

Un simbolo è un oggetto dati con un nome. Di solito il nome può essere usato per trovare l'oggetto:

|This is a Symbol|
this-is-also-a-symbol

(find-symbol "SIN")   ->  SIN

Poiché i simboli sono oggetti dati reali, possiamo testare se sono lo stesso oggetto:

(eq 'sin 'cos) -> NIL
(eq 'sin 'sin) -> T

Questo ci consente ad esempio di scrivere una frase con simboli:

(defvar *sentence* '(mary called tom to tell him the price of the book))

Ora possiamo contare il numero di THE nella frase:

(count 'the *sentence*) ->  2

In Common Lisp i simboli non solo hanno un nome, ma possono anche avere un valore, una funzione, un elenco di proprietà e un pacchetto. Quindi i simboli possono essere usati per nominare variabili o funzioni. L'elenco delle proprietà viene generalmente utilizzato per aggiungere metadati ai simboli.

3) Una notazione per il codice usando alberi di simboli.

Lisp utilizza le sue strutture di dati di base per rappresentare il codice.

L'elenco (* 3 2) può essere sia dati che codice:

(eval '(* 3 (+ 2 5))) -> 21

(length '(* 3 (+ 2 5))) -> 3

L'albero:

CL-USER 8 > (sdraw '(* 3 (+ 2 5)))

[*|*]--->[*|*]--->[*|*]--->NIL
 |        |        |
 v        v        v
 *        3       [*|*]--->[*|*]--->[*|*]--->NIL
                   |        |        |
                   v        v        v
                   +        2        5

4) L'intera lingua è sempre disponibile. Non esiste una reale distinzione tra tempo di lettura, tempo di compilazione e runtime. È possibile compilare o eseguire il codice durante la lettura, leggere o eseguire il codice durante la compilazione e leggere o compilare il codice in fase di esecuzione.

Lisp fornisce le funzioni READ per leggere dati e codice dal testo, LOAD per caricare il codice, EVAL per valutare il codice, COMPILE per compilare il codice e PRINT per scrivere dati e codice nel testo.

Queste funzioni sono sempre disponibili. Non vanno via. Possono far parte di qualsiasi programma. Ciò significa che qualsiasi programma può leggere, caricare, valutare o stampare il codice - sempre.

In che modo differiscono in lingue come C o Java?

Tali lingue non forniscono simboli, codice come dati o valutazione di runtime dei dati come codice. Gli oggetti dati in C sono in genere non tipizzati.

Altre lingue diverse dalle lingue della famiglia LISP hanno uno di questi costrutti ora?

Molte lingue hanno alcune di queste capacità.

La differenza:

In Lisp queste funzionalità sono progettate nella lingua in modo che siano facili da usare.


33

Per i punti (1) e (2), sta parlando storicamente. Le variabili di Java sono praticamente le stesse, motivo per cui è necessario chiamare .equals () per confrontare i valori.

(3) sta parlando di espressioni S. I programmi Lisp sono scritti in questa sintassi, che offre molti vantaggi rispetto alla sintassi ad-hoc come Java e C, come l'acquisizione di schemi ripetuti nelle macro in un modo molto più pulito rispetto ai macro C o ai modelli C ++ e la manipolazione del codice con lo stesso elenco di core operazioni utilizzate per i dati.

(4) prendendo ad esempio C: la lingua è in realtà due diverse lingue secondarie: cose come if () e while (), e il preprocessore. Si utilizza il preprocessore per evitare di dover ripetersi continuamente o per saltare il codice con # if / # ifdef. Ma entrambe le lingue sono abbastanza separate e non puoi usare while () in fase di compilazione come puoi #if.

C ++ rende ancora peggio con i template. Dai un'occhiata ad alcuni riferimenti sulla metaprogrammazione dei template, che fornisce un modo per generare codice in fase di compilazione ed è estremamente difficile per i non esperti avvolgere la testa. Inoltre, è davvero un mucchio di trucchi e trucchi che utilizzano modelli e macro per i quali il compilatore non può fornire un supporto di prima classe: se si commette un semplice errore di sintassi, il compilatore non è in grado di fornire un messaggio di errore chiaro.

Bene, con Lisp, hai tutto questo in una sola lingua. Usi le stesse cose per generare codice in fase di esecuzione mentre impari nel tuo primo giorno. Questo non significa che la metaprogrammazione sia banale, ma è certamente più semplice con il linguaggio di prima classe e il supporto del compilatore.


7
Oh, inoltre, questo potere (e semplicità) ha ormai più di 50 anni e abbastanza facile da implementare che un programmatore alle prime armi può batterlo con una guida minima e conoscere i fondamenti della lingua. Non sentiresti una simile affermazione di Java, C, Python, Perl, Haskell, ecc. Come un buon progetto per principianti!
Matt Curtis,

9
Non credo che le variabili Java siano come i simboli Lisp. Non c'è notazione per un simbolo in Java e l'unica cosa che puoi fare con una variabile è ottenere la sua cella di valore. Le stringhe possono essere internate ma in genere non sono nomi, quindi non ha nemmeno senso parlare se possono essere citate, valutate, passate, ecc.
Ken

2
Oltre 40 anni potrebbero essere più precisi :), @Ken: penso che significhi che 1) le variabili non primitive in java sono per riferimento, che è simile a lisp e 2) le stringhe internate in java sono simili ai simboli in lisp - ovviamente, come hai detto, non puoi citare o valutare stringhe / codice internati in Java, quindi sono ancora abbastanza diversi.

3
@ Dan - Non sono sicuro di quando la prima implementazione sia stata messa insieme, ma il primo documento McCarthy sul calcolo simbolico è stato pubblicato nel 1960.
Inaimathi,

Java ha un supporto parziale / irregolare per "simboli" sotto forma di Foo.class / foo.getClass () - vale a dire un oggetto di classe <Foo> di tipo tipo è un po 'analogo - come lo sono i valori enum, a un grado. Ma ombre minime di un simbolo di Lisp.
BRPocock

-3

Anche i punti (1) e (2) si adattano a Python. Prendendo un semplice esempio "a = str (82.4)" l'interprete crea prima un oggetto a virgola mobile con valore 82.4. Quindi chiama un costruttore di stringhe che quindi restituisce una stringa con valore '82 .4 '. La 'a' sul lato sinistro è semplicemente un'etichetta per quell'oggetto stringa. L'oggetto in virgola mobile originale è stato garbage collection perché non vi sono più riferimenti ad esso.

In Scheme tutto viene trattato come un oggetto in modo simile. Non sono sicuro di Common Lisp. Proverei a evitare di pensare in termini di concetti C / C ++. Mi hanno rallentato un sacco quando stavo cercando di aggirare la splendida semplicità di Lisps.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.