web-dev-qa-db-fra.com

Veuillez expliquer certains des points de Paul Graham sur Lisp

J'ai besoin d'aide pour comprendre certains des points de Paul Graham Qu'est-ce qui rendait LISP différent .

  1. Un nouveau concept de variables. Dans LISP, toutes les variables sont effectivement des pointeurs. Les valeurs sont ce qui a des types, pas des variables, et assigner ou lier des variables signifie copier des pointeurs, pas ce vers quoi 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 lors de l'exécution.

Que signifient ces points? En quoi sont-ils différents dans des langages comme C ou Java? Existe-t-il actuellement d'autres langages autres que les langages de la famille LISP?

143
unj2

L'explication de Matt est parfaitement bien - et il prend une photo par rapport à C et Java, ce que je ne ferai pas - mais pour une raison quelconque, j'aime vraiment discuter de ce sujet de temps en temps, alors - voici ma photo à une réponse.

Aux points (3) et (4):

Les points (3) et (4) de votre liste semblent les plus intéressants et toujours pertinents à l'heure actuelle.

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é 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 affiche aFOObFOOcFOO. Notez que Clojure ne répond sans doute pas entièrement au quatrième point de votre liste, car le temps de lecture n'est pas vraiment ouvert au code utilisateur; Je vais discuter de ce que cela signifierait pour qu'il en soit autrement, cependant.

Supposons donc 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 à (println et se termine au ) à l'extrême droite. C'est lexé/analysé comme on pourrait s'y attendre, mais déjà un point important se pose: le résultat n'est pas une représentation spéciale spécifique au compilateur AST représentation - c'est juste une structure de données régulière Clojure/LISP , à savoir une liste imbriquée contenant un tas de symboles, de chaînes et - dans ce cas - un seul objet de motif regex compilé correspondant au #"\d+" littéral (plus de détails ci-dessous). Certains Lisps ajoutent leurs propres petits détails à ce processus, mais Paul Graham faisait principalement référence à Common LISP. Sur les points pertinents à votre question, Clojure est similaire à CL.

La langue entière au moment de la compilation:

Après ce point, tout le compilateur traite (cela serait également vrai pour un interpréteur LISP; le code Clojure se trouve toujours être compilé) est des structures de données LISP que les programmeurs LISP sont habitués à manipuler. À ce stade, une merveilleuse possibilité apparaît: pourquoi ne pas permettre aux programmeurs LISP d'écrire des fonctions LISP qui manipulent des données LISP représentant des programmes LISP et qui 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 fonctionnant 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. Puisqu'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 ".

Toute la langue au moment de la lecture:

Revenons à cela #"\d+" littéral regex. Comme mentionné ci-dessus, cela se transforme en un véritable objet de modèle compilé 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 n hack intelligent . Dans Common LISP, l'histoire serait légèrement plus claire conceptuellement. Les bases sont cependant similaires: le LISP Reader est une machine à états qui, en plus d'effectuer des transitions d'état et de déclarer éventuellement s'il a atteint un "état d'acceptation", crache des structures de données LISP que les personnages représentent. Ainsi, les caractères 123 devenir le nombre 123 etc. Le point important vient maintenant: cette machine d'état peut être modifiée par le 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 que je suis censé développer, alors ...)

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 au lecteur une fonction pour réagir de manière appropriée à une séquence de caractères - [ ou #[ éventuellement - et le traiter comme le début d'un vecteur littéral se terminant à la correspondance ]. Une telle fonction est appelée 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 conclusion:

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 première étape à suivre 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 les fonctions LISP. Vous pouvez simplement appeler read ou eval à tout moment pour lire les données LISP à partir des flux de caractères ou compiler et exécuter le code LISP, respectivement. C'est toute la langue là, tout le temps.

Notez que le fait que LISP satisfasse 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 dépend fortement du 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 éventuellement avoir un LISP écrit en utilisant XML.

97
Michał Marczyk

1) ( Un nouveau concept de variables. Dans LISP, toutes les variables sont en fait des pointeurs. Les valeurs sont ce qui a des types, pas des variables, et assigner ou lier des variables signifie copier des pointeurs, pas quoi ils pointent vers.

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

'it' 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 leur "type".

(type-of "abc")  -> STRING

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

Dans 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. Les symboles peuvent donc ê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étadonné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) L'ensemble du langage est 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 pendant la lecture, la lecture ou l'exécution code lors de la compilation, et lire ou compiler du code lors de l'exécution.

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

Ces fonctions sont toujours disponibles. Ils ne s'en vont pas. Ils peuvent faire partie de n'importe quel programme. Cela signifie que tout 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 sont généralement non typés.

Existe-t-il maintenant d'autres langages autres que les langages de la famille LISP?

De nombreuses langues possèdent certaines de ces capacités.

La différence:

Dans LISP, ces capacités sont conçues dans la langue afin qu'elles soient faciles à utiliser.

65
Rainer Joswig

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, qui offre de nombreux avantages par rapport à la syntaxe ad hoc comme Java et C, comme la capture de motifs répétés dans les macros d'une manière beaucoup plus propre que les macros C ou les modèles C++ et la manipulation de code avec les mêmes opérations de liste de base 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 langues sont assez distinctes, et vous ne pouvez pas utiliser while () au moment de la compilation comme vous pouvez #if.

C++ aggrave encore la situation avec les modèles. Consultez 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 il est extrêmement difficile pour les non-experts de boucler la tête. De plus, c'est vraiment un tas de hacks et d'astuces utilisant des modèles et des macros que le compilateur ne peut pas fournir un support de première classe - si vous faites une simple erreur de syntaxe, le compilateur ne peut pas 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 avez appris lors de votre premier jour. Cela ne signifie pas que la métaprogrammation est triviale, mais elle est certainement plus simple avec le langage de première classe et le support du compilateur.

33
Matt Curtis