Homoiconicity, comment ça marche?

Quelqu’un peut-il proposer des articles qui expliquent le concept d’homoiconicité, en particulier en utilisant Clojure. Pourquoi est-ce que Clojure est homoiconique mais c’est difficile à faire dans d’autres langages comme Java?

Avant de continuer avec certaines choses, je voulais append une autre réponse à, voici une autre référence – la partie liée à l’homoiconicity est assez courte, mais c’est Rich Hickey qui fait l’explication! Channel 9 a cette belle vidéo avec Rich Hickey et Brian Beckman parlant de Clojure. La concurrence est, bien entendu, l’objective principal, mais l’homoiconicité a son propre moment d’écran pendant lequel Rich explique bien l’interaction entre read (la fonction qui convertit la syntaxe concrète écrite par le programmeur en représentation interne construite). des listes etc.) et eval . Il a ce joli diagramme qui montre à quel point eval ne sait même pas que le code qu’il évalue provient de la read opérant sur un fichier texte… Arthur a déjà expliqué l’essentiel derrière ça, mais bon, regarde-le quand même, c’est une très belle vidéo!


Une clause de non-responsabilité: je citerai Java et Python comme exemples sous la barre horizontale suivante. Je tiens à préciser que ce qui suit n’est qu’un aperçu des raisons pour lesquelles je pense qu’il serait difficile de créer un Java ou un Python homoiconique, de style Lisp-style; C’est juste un exercice académique, et je ne veux pas considérer la question de savoir s’il y a une raison d’essayer. De plus, je ne veux pas impliquer que la syntaxe d’un langage avec des macros de style Lisp doit contenir des délimiteurs explicites pour les arborescences ; Dylan (le Lisp sans paren) fournit apparemment un contre-exemple. Enfin, j’utilise l’expression macros de style Lisp car j’examine uniquement les macros de style Lisp. Le langage Forth, par exemple, a une facilité de macro différente que je ne comprends pas vraiment, sauf que je le sais pour activer le code de recherche cool. Apparemment, les extensions de syntaxe peuvent être implémentées de plusieurs manières. Avec ça à l’écart …


J’aimerais aborder la deuxième partie de votre question: comment se fait-il que la plupart des langages de programmation ne soient pas considérés comme homo? Je vais devoir aborder la sémantique de Lisp dans le processus, mais depuis que Nils a déjà fourni des liens vers de bonnes sources d’informations sur le terme “homoiconique” lui-même et qu’Arthur a décrit la lecture -> macro expand -> comstack cycle Dans Clojure, je vais en tirer parti dans ce qui suit. Pour commencer, permettez-moi de citer un passage d’Alan Kay (extrait de l’article de Wikipedia qui contient également des liens vers la source originale):

[…] Le LISP interactif et le […] TRAC sont tous deux […] “homo” dans la mesure où leurs représentations internes et externes sont essentiellement les mêmes.

(Ces bits cachent beaucoup de texte, mais l’essentiel rest inchangé.)

Maintenant, posons-nous la question: quelle est la représentation interne de Java par Java? … Eh bien, cela n’a même pas de sens. Le compilateur Java possède une certaine représentation interne de Java, à savoir un arbre de syntaxe abstrait; pour construire un “Java homoiconique”, il faudrait faire de cette représentation AST un object de première classe en Java et concevoir une syntaxe permettant d’écrire directement les AST. Cela pourrait s’avérer assez difficile.

Python fournit un exemple de langage non homoiconique intéressant en ce sens qu’il est actuellement livré avec un kit d’outils de manipulation AST sous la forme du module ast . Les documents de ce module indiquent explicitement que les AST Python peuvent changer entre les versions, ce qui peut ou non être décourageant. cependant, je suppose qu’un programmeur ast pourrait prendre le module ast , concevoir une syntaxe (peut-être basée sur S-expression, peut-être basée sur XML) pour décrire directement les Python AST et construire un parsingur pour cette syntaxe en utilisant Python premier pas vers la création d’un langage homoiconique avec la sémantique Python. (Je crois que je suis tombé sur un dialecte de Lisp compilant Python bytecode il y a quelque temps … Je me demande si cela pourrait faire quelque chose comme ça à un certain niveau?)

Même dans ce cas, le problème rest d’en tirer des avantages concrets. Il est considéré comme une propriété bénéfique des membres de la famille des langages Lisp, car il nous permet d’écrire des programmes qui écrivent d’autres programmes, parmi lesquels les macros sont les plus remarquables. Maintenant que les macros sont activées d’une certaine manière par le fait qu’il est si facile de manipuler la représentation interne du code Lisp dans Lisp, elles sont également activées de la même manière par le modèle d’exécution Lisp : un programme Lisp n’est qu’une collection des formes Lisp; ceux-ci sont traités par la fonction Lisp eval qui est chargée de déterminer les valeurs des expressions et de provoquer les effets secondaires appropriés au bon moment; la sémantique de Lisp est exactement la sémantique de eval . La question de savoir comment les choses fonctionnent en interne pour préserver cette illusion sémantique tout en étant raisonnablement rapide est un détail d’implémentation; un système Lisp a l’obligation d’exposer une fonction eval au programmeur et d’agir comme si les programmes Lisp étaient traités par cette fonction.

Dans les systèmes Lisp modernes, cela fait partie du contrat d’ eval qu’il effectue une phase de prétraitement supplémentaire au cours de laquelle les macros sont développées avant d’évaluer le code (ou la compilation et l’exécution, selon le cas). Cette fonctionnalité particulière n’est pas une partie nécessaire d’un système Lisp, mais il est tellement facile de le twigr sur ce modèle d’exécution! Aussi, je me demande si ce n’est pas le seul modèle d’exécution qui rend le type de transformations de macro de type Lisp gérable, ce qui signifierait que tout langage cherchant à incorporer des macros de style Lisp devrait adopter un modèle d’exécution similaire. Mon intuition me dit que c’est bien le cas.

Bien sûr, une fois qu’un langage est écrit en notation en parallèle de ses AST et utilise un modèle d’exécution de type Lisp avec une fonction / un object évaluateur, il faut se demander si ce n’est pas un autre dialecte de Lisp … La syntaxe de parallélisation AST se trouve être basée sur XML. frémir

Quand j’apprenais Lisp, l’idée de l’homoiconicité avait du sens quand j’ai appris que le lisp est “compilé” en deux phases, la lecture et la compilation et le code est représenté avec la même structure de données pour les deux:

  • D’abord, vous pensez à une expression s dans votre tête
  • puis vous tapez l’expression s en tant que caractères dans un fichier
  • le lecteur traduit ensuite les caractères du fichier en expressions-s. Il ne s’agit pas de comstackr le programme, il suffit de construire des structures de données à partir de caractères, cela fait partie de la phase de lecture.
  • Ensuite, le lecteur examine chacune des expressions et décide si elles sont une macro et si oui, exécute la macro pour produire une autre expression s. Nous sums donc passés des expressions-s à des caractères à des expressions-s, puis à des expressions-s à des expressions-s différentes.
  • ces expressions s sont ensuite compilées dans des fichiers .class qui peuvent être exécutés par le jvm. Il s’agit de la deuxième phase de la “compilation”.

Donc, ce sont à peu près les s-expressions de votre cerveau au fichier .class. Vous écrivez même des expressions S qui écrivent des expressions S. vous pouvez donc dire que “le code est la donnée” ou “le code est la donnée” car cela sonne mieux.

L’idée même de «homoiconicity» est légèrement confuse et ne s’intègre pas bien dans Lisp. Les représentations internes et externes ne sont pas les mêmes dans Lisp. La représentation externe est basée sur les caractères des fichiers. La représentation interne est basée sur les données Lisp (nombres, chaînes, listes, tableaux, etc.) et n’est pas textuelle. Comment est-ce la même chose que les personnages? Il y a des représentations internes, qui n’ont pas de représentations externes correspondantes (par exemple, code de compilation, fermetures, …).

La principale différence entre Lisp et de nombreux autres langages de programmation est que Lisp possède une représentation de données simple pour le code source – une qui ne repose pas sur des chaînes de caractères.

De toute évidence, le code peut être représenté sous forme de chaînes dans les langages de programmation textuels. Mais dans Lisp, la source peut être représentée en termes de structures de données primitives Lisp. La représentation externe est basée sur des expressions s, qui sont un modèle simple pour représenter des données hiérarchiques sous forme de texte. Le modèle interne est la représentation basée sur des listes, etc.

C’est ce que l’évaluateur obtient: des représentations internes. Pas 1 à 1 versions de l’entrée textuelle, mais analysées.

Le modèle de base:

  • READ traduit des expressions S externes en données
  • EVAL prend les formes Lisp sous forme de données Lisp et les évalue
  • PRINT traduit les données Lisp en expressions S externes

Notez que READ et PRINT fonctionnent pour des données Lisp arbitraires, qui ont une représentation imprimée et un lecteur, et pas seulement pour les formulaires Lisp. Les formulaires sont par définition des expressions valides dans le langage de programmation Lisp.

Voici un programme court pour faire la différenciation symbolique. Voici un exemple de manipulation par le LISP de son propre code. Essayez de le traduire dans une autre langue pour voir pourquoi LISP est bon pour ce genre de chose.

 ;; The simplest possible symbolic differentiator ;; Functions to create and unpack additions like (+ 1 2) (defn make-add [ ab ] (list '+ ab)) (defn addition? [x] (and (=(count x) 3) (= (first x) '+))) (defn add1 [x] (second x)) (defn add2 [x] (second (rest x))) ;; Similar for multiplications (* 1 2) (defn make-mul [ ab ] (list '* ab)) (defn multiplication? [x] (and (=(count x) 3) (= (first x) '*))) (defn mul1 [x] (second x)) (defn mul2 [x] (second (rest x))) ;; Differentiation. (defn deriv [exp var] (cond (number? exp) 0 ;; d/dx c -> 0 (symbol? exp) (if (= exp var) 1 0) ;; d/dx x -> 1, d/dx y -> 0 (addition? exp) (make-add (deriv (add1 exp) var) (deriv (add2 exp) var)) ;; d/dx a+b -> d/dx a + d/dx b (multiplication? exp) (make-add (make-mul (deriv (mul1 exp) var) (mul2 exp)) ;; d/dx a*b -> d/dx a * b + a * d/dx b (make-mul (mul1 exp) (deriv (mul2 exp) var))) :else :error)) ;;an example of use: create the function x -> x^3 + 2x^2 + 1 and its derivative (def poly '(+ (+ (* x (* xx)) (* 2 (* xx))) 1)) (defn poly->fnform [poly] (list 'fn '[x] poly)) (def polyfn (eval (poly->fnform poly))) (def dpolyfn (eval (poly->fnform (deriv poly 'x)))) 

Cela semble presque évident, mais les premières sources pourraient être:

http://en.wikipedia.org/wiki/Homoiconicity

http://c2.com/cgi/wiki?DefinitionOfHomoiconic

L’homoiconicité est expliquée en général et vous pouvez également trouver les sources d’origine. Comme cela est expliqué en utilisant l’exemple de Lisp, ce n’est pas si loin de Clojure.

Comme le fait remarquer Rainer Joswig, il y a de bonnes raisons de douter de l’utilité de l’idée d’homoiconicité et de savoir si les Lisps sont en réalité homoiconiques.

La définition originale de l’homoiconiticy est centrée sur une similitude entre les représentations internes et externes d’une langue . L’exemple canonique est Lisp, avec ses expressions-s.

Il y a (au moins) deux problèmes avec cette définition et le choix de l’exemple.

La première objection concerne la représentation externe. Dans le cas de Lisp, nous supposons que la représentation externe est une expression s. Dans la plupart des environnements de programmation pratiques, la représentation réelle des sources de programme se présente sous la forme de fichiers texte contenant des chaînes de caractères. Ce n’est qu’après avoir analysé ce texte que la représentation est vraiment une expression s. En d’autres termes: dans des environnements pratiques, la représentation externe n’est pas une expression s, mais du texte.

La deuxième objection concerne la représentation interne. Les implémentations pratiques des interpréteurs Lisp ne fonctionnent généralement pas directement sur les expressions S en interne pour des raisons de performances. Même si une Lisp peut être définie en termes d’parsing de cas sur des expressions s, elle n’est généralement pas implémentée comme telle. Ainsi, la représentation interne n’est pas réellement une expression s en pratique.

En fait, on pourrait même soulever d’autres questions autour du concept d’homoiconicité: pour une machine bien encapsulée, nous ne pouvons pas observer son fonctionnement interne par définition; dans cette vue, toute déclaration sur la représentation interne de la machine n’a pas de sens. Plus généralement, la définition initiale pose le problème que l’idée qu’il existe une seule représentation externe et une seule représentation interne du programme ne correspond pas à la réalité. En fait, il y a toute une chaîne de représentations, y compris des électrons dans le cerveau du programmeur, des photons émis à partir de l’écran, du texte de programme, du code machine et des électrons se déplaçant dans la CPU.

J’ai écrit à ce sujet plus en détail dans un article intitulé Don’t say “Homoiconic”