Veuillez expliquer certains des points de Paul Graham sur Lisp

146

J'ai besoin d'aide pour comprendre certains des points de What Made Lisp Different de Paul Graham .

  1. Un nouveau concept de variables. En Lisp, toutes les variables sont en fait des pointeurs. Les valeurs sont ce qui a des types, pas des variables, et l'affectation ou la liaison de variables signifie copier des pointeurs, pas ce qu'ils pointent.

  2. Un type de symbole. Les symboles diffèrent des chaînes en ce que vous pouvez tester l'égalité en comparant un pointeur.

  3. Une notation pour le code utilisant des arbres de symboles.

  4. La langue entière toujours disponible. Il n'y a pas de réelle distinction entre le temps de lecture, le temps de compilation et le temps d'exécution. Vous pouvez compiler ou exécuter du code lors de la lecture, lire ou exécuter du code lors de la compilation, et lire ou compiler du code au moment de l'exécution.

Que signifient ces points? En quoi sont-ils différents dans des langages comme C ou Java? Est-ce que d'autres langages autres que les langages de la famille Lisp ont l'une de ces constructions maintenant?

unj2
la source
10
Je ne suis pas sûr que la balise de programmation fonctionnelle soit justifiée ici, car il est tout aussi possible d'écrire du code impératif ou OO dans de nombreux Lisps que d'écrire du code fonctionnel - et en fait il y a beaucoup de Lisp non fonctionnel code autour. Je suggère que vous supprimiez la balise fp et que vous ajoutiez plutôt clojure - j'espère que cela pourrait apporter une contribution intéressante de Lispers basés sur JVM.
Michał Marczyk
58
Nous avons aussi une paul-grahambalise ici? !!! Great ...
missingfaktor
@missingfaktor Peut-être qu'il a besoin d'une demande de burninate
chat

Réponses:

98

L'explication de Matt est parfaitement correcte - et il tente une comparaison avec C et Java, ce que je ne ferai pas - mais pour une raison quelconque, j'aime vraiment discuter de ce sujet même de temps en temps, alors - voici ma photo à une réponse.

Sur les points (3) et (4):

Les points (3) et (4) de votre liste semblent les plus intéressants et toujours d'actualité.

Pour les comprendre, il est utile d'avoir une image claire de ce qui se passe avec le code Lisp - sous la forme d'un flux de caractères tapés par le programmeur - en cours d'exécution. Prenons un exemple concret:

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

Cet extrait de code Clojure s'imprime aFOObFOOcFOO. Notez que Clojure ne satisfait sans doute pas entièrement le quatrième point de votre liste, car le temps de lecture n'est pas vraiment ouvert au code utilisateur; Je vais cependant discuter de ce que cela signifierait pour cela.

Donc, supposons que nous ayons ce code dans un fichier quelque part et que nous demandions à Clojure de l'exécuter. Supposons également (par souci de simplicité) que nous avons dépassé l'importation de la bibliothèque. Le bit intéressant commence à (printlnet se termine à l' )extrême droite. Ceci est lexé / analysé comme on pouvait s'y attendre, mais déjà un point important se pose: le résultat n'est pas une représentation AST spéciale spécifique au compilateur - c'est juste une structure de données Clojure / Lisp régulière , à savoir une liste imbriquée contenant un tas de symboles, strings et - dans ce cas - un seul objet de motif regex compilé correspondant au#"\d+"littéral (plus à ce sujet ci-dessous). Certains Lisps ajoutent leurs propres petits rebondissements à ce processus, mais Paul Graham faisait surtout référence à Common Lisp. Sur les points pertinents à votre question, Clojure est similaire à CL.

L'ensemble du langage au moment de la compilation:

Après ce point, tout le compilateur s'occupe (ce serait également vrai pour un interpréteur Lisp; le code Clojure est toujours compilé) ce sont les structures de données Lisp que les programmeurs Lisp sont habitués à manipuler. À ce stade, une possibilité merveilleuse devient évidente: pourquoi ne pas permettre aux programmeurs Lisp d'écrire des fonctions Lisp qui manipulent des données Lisp représentant des programmes Lisp et produisent des données transformées représentant des programmes transformés, à utiliser à la place des originaux? En d'autres termes - pourquoi ne pas autoriser les programmeurs Lisp à enregistrer leurs fonctions en tant que plugins de compilation, appelés macros en Lisp? Et en effet, tout système Lisp décent a cette capacité.

Ainsi, les macros sont des fonctions Lisp régulières opérant sur la représentation du programme au moment de la compilation, avant la phase de compilation finale lorsque le code objet réel est émis. Comme il n'y a pas de limites sur les types de code que les macros sont autorisées à exécuter (en particulier, le code qu'elles exécutent est souvent lui-même écrit avec une utilisation libérale de la fonction macro), on peut dire que "tout le langage est disponible au moment de la compilation ".

La langue entière à la lecture:

Revenons à ce #"\d+"littéral regex. Comme mentionné ci-dessus, cela est transformé en un objet modèle compilé réel au moment de la lecture, avant que le compilateur n'entende la première mention du nouveau code en cours de préparation pour la compilation. Comment cela peut-il arriver?

Eh bien, la façon dont Clojure est actuellement implémenté, l'image est quelque peu différente de ce que Paul Graham avait en tête, bien que tout soit possible avec un hack intelligent . En Common Lisp, l'histoire serait légèrement plus claire sur le plan conceptuel. Les bases sont cependant similaires: le Lisp Reader est une machine à états qui, en plus d'effectuer des transitions d'états et éventuellement de déclarer s'il a atteint un "état d'acceptation", crache les structures de données Lisp que les caractères représentent. Ainsi les caractères 123deviennent le nombre 123etc. Le point important vient maintenant: cette machine d'état peut être modifiée par code utilisateur. (Comme indiqué précédemment, c'est tout à fait vrai dans le cas de CL; pour Clojure, un hack (découragé et non utilisé dans la pratique) est nécessaire. Mais je m'éloigne du sujet, c'est l'article de PG sur lequel je suis censé élaborer, donc ...)

Donc, si vous êtes un programmeur Common Lisp et que vous aimez l'idée des littéraux vectoriels de style Clojure, vous pouvez simplement brancher dans le lecteur une fonction pour réagir de manière appropriée à une séquence de caractères - [ou #[éventuellement - et la traiter comme le début d'un littéral vectoriel se terminant à la correspondance ]. Une telle fonction s'appelle une macro de lecture et, tout comme une macro ordinaire, elle peut exécuter n'importe quel type de code Lisp, y compris du code qui a lui-même été écrit avec une notation géniale activée par des macros de lecteur précédemment enregistrées. Il y a donc toute la langue au moment de la lecture pour vous.

En résumé:

En fait, ce qui a été démontré jusqu'à présent, c'est que l'on peut exécuter des fonctions Lisp régulières au moment de la lecture ou de la compilation; la seule étape à franchir pour comprendre comment la lecture et la compilation sont elles-mêmes possibles au moment de la lecture, de la compilation ou de l'exécution est de réaliser que la lecture et la compilation sont elles-mêmes effectuées par des fonctions Lisp. Vous pouvez simplement appeler readou evalà tout moment lire des données Lisp à partir de flux de caractères ou compiler et exécuter du code Lisp, respectivement. C'est tout le langage ici, tout le temps.

Notez comment le fait que Lisp satisfait le point (3) de votre liste est essentiel à la façon dont il parvient à satisfaire le point (4) - la saveur particulière des macros fournies par Lisp repose fortement sur le code représenté par des données Lisp régulières, ce qui est activé par (3). Soit dit en passant, seul l'aspect "arborescent" du code est vraiment crucial ici - vous pourriez imaginer avoir un Lisp écrit en XML.

Michał Marczyk
la source
4
Attention: en disant "macro régulière (compilateur)", vous êtes sur le point d'impliquer que les macros de compilateur sont des macros "régulières", alors qu'en Common Lisp (au moins), "macro compilateur" est une chose très spécifique et différente: lispworks. com / documentation / lw51 / CLHS / Body /…
Ken
Ken: Bonne prise, merci! Je vais changer cela en "macro standard", ce qui, je pense, ne risque pas de trébucher.
Michał Marczyk
Réponse fantastique. J'en ai appris plus en 5 minutes qu'en heures de googler / réfléchir à la question. Merci.
Charlie Flowers
Edit: argh, a mal compris une phrase de rupture. Corrigé pour la grammaire (besoin d'un "pair" pour accepter ma modification).
Tatiana Racheva
Les expressions S et XML peuvent dicter les mêmes structures, mais XML est beaucoup plus verbeux et ne convient donc pas comme syntaxe.
Sylwester
66

1) Un nouveau concept de variables. En Lisp, toutes les variables sont en fait des pointeurs. Les valeurs sont ce qui a des types, pas des variables, et l'affectation ou la liaison de variables signifie copier des pointeurs, pas ce qu'ils pointent.

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

«c'est» est une variable. Il peut être lié à N'IMPORTE QUELLE valeur. Il n'y a aucune restriction et aucun type associé à la variable. Si vous appelez la fonction, l'argument n'a pas besoin d'être copié. La variable est similaire à un pointeur. Il a un moyen d'accéder à la valeur qui est liée à la variable. Il n'est pas nécessaire de réserver de la mémoire. Nous pouvons passer n'importe quel objet de données lorsque nous appelons la fonction: n'importe quelle taille et n'importe quel type.

Les objets de données ont un «type» et tous les objets de données peuvent être interrogés pour son «type».

(type-of "abc")  -> STRING

2) Un type de symbole. Les symboles diffèrent des chaînes en ce que vous pouvez tester l'égalité en comparant un pointeur.

Un symbole est un objet de données avec un nom. Habituellement, le nom peut être utilisé pour trouver l'objet:

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

(find-symbol "SIN")   ->  SIN

Puisque les symboles sont de vrais objets de données, nous pouvons tester s'ils sont le même objet:

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

Cela nous permet par exemple d'écrire une phrase avec des symboles:

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

Maintenant, nous pouvons compter le nombre de THE dans la phrase:

(count 'the *sentence*) ->  2

En Common Lisp, les symboles ont non seulement un nom, mais ils peuvent également avoir une valeur, une fonction, une liste de propriétés et un package. Ainsi, les symboles peuvent être utilisés pour nommer des variables ou des fonctions. La liste des propriétés est généralement utilisée pour ajouter des méta-données aux symboles.

3) Une notation pour le code utilisant des arbres de symboles.

Lisp utilise ses structures de données de base pour représenter le code.

La liste (* 3 2) peut être à la fois des données et du code:

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

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

L'arbre:

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

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

4) Toute la langue toujours disponible. Il n'y a pas de réelle distinction entre le temps de lecture, le temps de compilation et le temps d'exécution. Vous pouvez compiler ou exécuter du code lors de la lecture, lire ou exécuter du code lors de la compilation, et lire ou compiler du code au moment de l'exécution.

Lisp fournit les fonctions READ pour lire des données et du code à partir de texte, LOAD pour charger du code, EVAL pour évaluer le code, COMPILE pour compiler du code et PRINT pour écrire des données et du code dans du texte.

Ces fonctions sont toujours disponibles. Ils ne s'en vont pas. Ils peuvent faire partie de n'importe quel programme. Cela signifie que n'importe quel programme peut lire, charger, évaluer ou imprimer du code - toujours.

En quoi sont-ils différents dans des langages comme C ou Java?

Ces langages ne fournissent pas de symboles, de code sous forme de données ou d'évaluation d'exécution des données sous forme de code. Les objets de données en C ne sont généralement pas typés.

Est-ce que d'autres langues autres que les langues de la famille LISP ont l'une de ces constructions maintenant?

De nombreuses langues ont certaines de ces capacités.

La différence:

En Lisp, ces capacités sont conçues dans le langage afin d'être faciles à utiliser.

Rainer Joswig
la source
33

Pour les points (1) et (2), il parle historiquement. Les variables de Java sont à peu près les mêmes, c'est pourquoi vous devez appeler .equals () pour comparer les valeurs.

(3) parle d'expressions S. Les programmes Lisp sont écrits dans cette syntaxe, ce qui offre de nombreux avantages par rapport à la syntaxe ad hoc comme Java et C, tels que la capture de modèles répétés dans des macros d'une manière beaucoup plus propre que les macros C ou les modèles C ++, et la manipulation du code avec la même liste de base opérations que vous utilisez pour les données.

(4) en prenant C par exemple: le langage est en réalité deux sous-langages différents: des trucs comme if () et while (), et le préprocesseur. Vous utilisez le préprocesseur pour éviter d'avoir à vous répéter tout le temps, ou pour sauter le code avec # if / # ifdef. Mais les deux langages sont assez séparés et vous ne pouvez pas utiliser while () au moment de la compilation comme vous pouvez le faire #if.

C ++ rend cela encore pire avec les modèles. Découvrez quelques références sur la métaprogrammation de modèles, qui fournit un moyen de générer du code au moment de la compilation, et est extrêmement difficile pour les non-experts de comprendre. De plus, c'est vraiment un tas de hacks et d'astuces utilisant des modèles et des macros pour lesquels le compilateur ne peut pas fournir un support de première classe - si vous faites une simple erreur de syntaxe, le compilateur est incapable de vous donner un message d'erreur clair.

Eh bien, avec Lisp, vous avez tout cela dans une seule langue. Vous utilisez les mêmes éléments pour générer du code au moment de l'exécution que vous apprenez le premier jour. Cela ne veut pas dire que la métaprogrammation est triviale, mais elle est certainement plus simple avec un langage de première classe et le support du compilateur.

Matt Curtis
la source
7
Oh aussi, ce pouvoir (et cette simplicité) a maintenant plus de 50 ans et est suffisamment facile à mettre en œuvre pour qu'un programmeur novice puisse le faire avec un minimum de conseils et en apprendre davantage sur les principes fondamentaux du langage. Vous n'entendriez pas une affirmation similaire de Java, C, Python, Perl, Haskell, etc. comme un bon projet pour débutant!
Matt Curtis
9
Je ne pense pas du tout que les variables Java soient comme des symboles Lisp. Il n'y a pas de notation pour un symbole en Java, et la seule chose que vous pouvez faire avec une variable est d'obtenir sa cellule de valeur. Les chaînes peuvent être internées mais ce ne sont généralement pas des noms, il n'est donc même pas logique de dire si elles peuvent être citées, évaluées, passées, etc.
Ken
2
Plus de 40 ans pourrait être plus précis :), @Ken: Je pense qu'il veut dire que 1) les variables non primitives en java sont par référence, ce qui est similaire à lisp et 2) les chaînes internées en java sont similaires aux symboles de lisp - Bien sûr, comme vous l'avez dit, vous ne pouvez pas citer ou évaluer les chaînes / codes internes en Java, ils sont donc toujours très différents.
3
@Dan - Je ne sais pas quand la première implémentation a été mise en place, mais le premier article de McCarthy sur le calcul symbolique a été publié en 1960.
Inaimathi
Java a un support partiel / irrégulier pour les «symboles» sous la forme de Foo.class / foo.getClass () - c'est-à-dire qu'un objet Class <Foo> de type est un peu analogue - tout comme les valeurs enum, pour un diplôme. Mais des ombres très minimes d'un symbole Lisp.
BRPocock
-3

Les points (1) et (2) conviendraient également à Python. En prenant un exemple simple "a = str (82.4)", l'interpréteur crée d'abord un objet à virgule flottante avec la valeur 82.4. Ensuite, il appelle un constructeur de chaîne qui retourne ensuite une chaîne avec la valeur '82 .4 '. Le «a» sur le côté gauche est simplement une étiquette pour cet objet chaîne. L'objet à virgule flottante d'origine a été récupéré car il n'y a plus de références à celui-ci.

Dans Scheme, tout est traité comme un objet de la même manière. Je ne suis pas sûr de Common Lisp. J'essaierais d'éviter de penser en termes de concepts C / C ++. Ils m'ont ralenti des tas quand j'essayais de comprendre la belle simplicité de Lisps.

CyberFonic
la source