Maison > Article > développement back-end > Pretty-Printing est une compilation
Une plus jolie imprimante de Wadler est une perle fonctionnelle classique. Cependant, que ce soit à cause de la paresse de Haskell ou de la mienne, j'ai eu du mal à réimplémenter ses idées dans d'autres langues (ou même en Haskell, cinq minutes après avoir lu l'article). Heureusement, Lindig a reconnu ce problème et a enflammé les masses dans Strictly Pretty. Pourtant, même cela n’était pas assez stupide pour moi.
Mais après avoir passé un peu plus de temps à bricoler les idées des deux articles, je pense que j'ai enfin compris l'idée.
Comme le titre l'indique, nous considérerons le joli-printing comme un processus de compilation (et d'exécution) de programmes écrits dans un « langage de document » abstrait. Comme la plupart des langages de programmation, ce langage de document, que nous appellerons
Doc : contient des expressions qui peuvent être composées ensemble ; cela permet aux humains de raisonner facilement. Nous compilerons des expressions dans Doc en instructions dans une sorte de langage assembleur (ASM). Les instructions en ASM sont beaucoup plus faciles à transformer en chaînes.
Voici une vue schématique du processus :
À titre d'exemple concret, supposons que nous souhaitions imprimer joliment la liste imbriquée :
['onions', ['carrots', 'celery'], 'turnips']
Voici un programme Doc pour ce faire :
group( '[' + nest( 4, br() + "'onions'" + ',' + br(' ') + group( '[' + nest(4, br() + "'carrots'" + ',' + br(' ') + "'celery'") + br() + ']' ) + ',' + br(' ') + "'turnips'" ) + br() + ']' )
Nous rencontrerons le groupe, le nid, etc. sous peu. Pour l'instant, il suffit d'avoir une idée générale du langage du document.
Ce programme est ensuite compilé (avec certains « paramètres architecturaux » définis) dans les instructions ASM :
TEXT '[' LINE 4 TEXT "'onions'" TEXT ',' LINE 4 TEXT '[' TEXT '' TEXT "'carrots'" TEXT ',' TEXT ' ' TEXT "'celery'" TEXT '' TEXT ']' TEXT ',' LINE 4 TEXT "'turnips'" LINE 0 TEXT ']'
qui sont ensuite interprétés comme une chaîne :
[ 'onions', ['carrots', 'celery'], 'turnips' ]
Une caractéristique importante évoquée ci-dessus est que le compilateur dispose d'un "paramètre architectural" configurable, qui est une largeur de ligne maximale cible. En fonction de la valeur de ce paramètre, le compilateur émettra différentes instructions ASM pour un même programme Doc. Dans l'exemple ci-dessus, nous avons utilisé une largeur cible de 30. Si nous avions utilisé 20 à la place, les instructions d'assemblage émises seraient différentes, tout comme la chaîne résultante :
[ 'onions', [ 'carrots', 'celery' ], 'turnips' ]
Et si on en avait utilisé 60, ce serait :
['onions', ['carrots', 'celery'], 'turnips']
Le langage assembleur est simple, nous allons donc l'aborder en premier. Nous considérerons les instructions ASM comme contrôlant un périphérique d'impression très simple qui n'est capable de faire que deux choses :
ASM se compose donc de seulement deux instructions :
Les programmes ASM sont interprétés comme des chaînes en exécutant leurs instructions, les unes après les autres. A titre d'exemple, retraçons l'exécution du programme :
['onions', ['carrots', 'celery'], 'turnips']
Nous utiliserons un > pour indiquer l'instruction en cours d'exécution et afficher la sortie actuelle ci-dessous. Le caractère ^ indiquera l'emplacement actuel de la « tête d'impression ». Nous utiliserons également des caractères _ pour indiquer les espaces, car ceux-ci sont autrement difficiles à suivre.
La première instruction TEXT provoque l'émission de la chaîne 'hello' :
group( '[' + nest( 4, br() + "'onions'" + ',' + br(' ') + group( '[' + nest(4, br() + "'carrots'" + ',' + br(' ') + "'celery'") + br() + ']' ) + ',' + br(' ') + "'turnips'" ) + br() + ']' )
LIGNE 2 passe ensuite à la ligne suivante et indente la tête de 2 espaces :
TEXT '[' LINE 4 TEXT "'onions'" TEXT ',' LINE 4 TEXT '[' TEXT '' TEXT "'carrots'" TEXT ',' TEXT ' ' TEXT "'celery'" TEXT '' TEXT ']' TEXT ',' LINE 4 TEXT "'turnips'" LINE 0 TEXT ']'
Ensuite, TEXT 'indented' provoque l'ajout de 'indented' :
[ 'onions', ['carrots', 'celery'], 'turnips' ]
Suivi de 'monde', en raison du TEXTE 'monde' :
[ 'onions', [ 'carrots', 'celery' ], 'turnips' ]
LINE 0 fait avancer l'imprimante à la ligne suivante (et ne met pas du tout en retrait) :
['onions', ['carrots', 'celery'], 'turnips']
Et enfin, TEXT 'goodbye' émet 'goodbye' :
TEXT 'hello' LINE 2 TEXT 'indented' TEXT ' world' LINE 0 TEXT 'goodbye'
Nous représenterons les instructions ASM comme un « type de somme » :
C'est-à-dire :
> TEXT 'hello' LINE 2 TEXT 'indented' TEXT ' world' LINE 0 TEXT 'goodbye' == OUTPUT == hello ^
Interpréter une liste d'AsmInsts dans la chaîne qu'ils représentent est simplement une question de parcourir les instructions et de "faire ce qu'il faut":
TEXT 'hello' > LINE 2 TEXT 'indented' TEXT ' world' LINE 0 TEXT 'goodbye' == OUTPUT == hello __ ^
Pour les instructions TEXTE, l'interprète ajoute le texte au résultat ; pour les instructions LINE, l'interpréteur ajoute une nouvelle ligne (« n ») suivie d'espaces de retrait.
Nous pouvons tester l'interprétation avec les instructions ASM de l'exemple ci-dessus, traduites en Python :
TEXT 'hello' LINE 2 > TEXT 'indented' TEXT ' world' LINE 0 TEXT 'goodbye' == OUTPUT == hello __indented ^
Nous aimons l'ASM car il est facile à interpréter. Mais c'est pénible à utiliser. Cela motive le langage Doc plus convivial. Alors que les programmes ASM sont des séquences d'instructions, les programmes Doc sont des compositions de expressions. Ces expressions sont résumées par la grammaire suivante :
TEXT 'hello' LINE 2 TEXT 'indented' > TEXT ' world' LINE 0 TEXT 'goodbye' == OUTPUT == hello __indented world ^
Par exemple :
TEXT 'hello' LINE 2 TEXT 'indented' TEXT ' world' > LINE 0 TEXT 'goodbye' == OUTPUT == hello __indented world ^
est une expression Doc, telle quelle :
TEXT 'hello' LINE 2 TEXT 'indented' TEXT ' world' LINE 0 > TEXT 'goodbye' == OUTPUT == hello __indented world goodbye ^
Que représentent-ils ?
Donc, par exemple :
AsmInst = str | int
représente la chaîne :
def interpret(insts: list[AsmInst]) -> str: """Interpret the ASM instructions as a string.""" result = "" for inst in insts: match inst: case text if isinstance(text, str): result += inst case indent if isinstance(indent, int): result += f"\n{' ' * indent}" return result
La deuxième expression, plus complexe :
['onions', ['carrots', 'celery'], 'turnips']
peut représenter soit :
group( '[' + nest( 4, br() + "'onions'" + ',' + br(' ') + group( '[' + nest(4, br() + "'carrots'" + ',' + br(' ') + "'celery'") + br() + ']' ) + ',' + br(' ') + "'turnips'" ) + br() + ']' )
ou :
TEXT '[' LINE 4 TEXT "'onions'" TEXT ',' LINE 4 TEXT '[' TEXT '' TEXT "'carrots'" TEXT ',' TEXT ' ' TEXT "'celery'" TEXT '' TEXT ']' TEXT ',' LINE 4 TEXT "'turnips'" LINE 0 TEXT ']'
en fonction de la valeur du "paramètre architectural" de largeur de ligne maximale cible fournie au compilateur. Ainsi, les expressions br peuvent être traitées comme des sauts de ligne ou du texte normal, auquel cas leur valeur de texte est utilisée (ou '' si aucun argument de texte n'a été fourni).
Nous représenterons les expressions Doc en utilisant des classes strs et Python. En particulier :
[ 'onions', ['carrots', 'celery'], 'turnips' ]
Et DocExpr DocExpr ? Nous représenterons ceux qui utilisent une classe Concat supplémentaire :
[ 'onions', [ 'carrots', 'celery' ], 'turnips' ]
Nous souhaitons prendre en charge l'utilisation pour combiner des expressions, nous devons donc implémenter __add__ et __radd__ sur chacune des classes variantes. L'ajout de deux expressions Doc en utilisant simplement construit un Concat des deux. C'est assez simple de le faire manuellement, par exemple :
['onions', ['carrots', 'celery'], 'turnips']
Mais on peut s'épargner de la saisie en définissant un décorateur pour le faire à notre place :
TEXT 'hello' LINE 2 TEXT 'indented' TEXT ' world' LINE 0 TEXT 'goodbye'
Les classes variantes ressemblent désormais à :
> TEXT 'hello' LINE 2 TEXT 'indented' TEXT ' world' LINE 0 TEXT 'goodbye' == OUTPUT == hello ^
Notre tâche consiste maintenant à écrire un compilateur qui traduit les expressions du langage Doc en instructions ASM équivalentes, compte tenu d'une certaine largeur de ligne maximale cible :
TEXT 'hello' > LINE 2 TEXT 'indented' TEXT ' world' LINE 0 TEXT 'goodbye' == OUTPUT == hello __ ^
Cependant, il s'avère plus simple de "réduire" d'abord les expressions Doc en expressions dans un langage de Représentation intermédiaire (IR), puis de compiler les expressions IR dans ASM. Introduit ce "pass" supplémentaire rend chaque étape plus claire.
Notre schéma décrivant le processus de jolie impression était donc un peu trop simpliste. Voici l'image complète :
Les expressions IR ressemblent aux expressions Doc à bien des égards :
TEXT 'hello' LINE 2 > TEXT 'indented' TEXT ' world' LINE 0 TEXT 'goodbye' == OUTPUT == hello __indented ^
La principale différence est que nous n'avons plus d'expressions nulles : celles-ci sont converties en listes d'expressions IR dans le processus d'abaissement. En fait, c'est tout que fait la passe de descente :
TEXT 'hello' LINE 2 TEXT 'indented' > TEXT ' world' LINE 0 TEXT 'goodbye' == OUTPUT == hello __indented world ^
Nous devons d’abord définir IrExprs.
Cela devrait vous sembler familier :
TEXT 'hello' LINE 2 TEXT 'indented' TEXT ' world' > LINE 0 TEXT 'goodbye' == OUTPUT == hello __indented world ^
Tout ce que Lower fait est de remplacer les instances Nil() par une liste vide ([]) et les instances Concat(car, cdr) en ajoutant les résultats de la réduction des expressions car et cdr. Le même traitement est appliqué aux sous-expressions dans Nest et Group. Ce n'est rien de plus qu'une opération "d'aplatissement" récursive.
TEXT 'hello' LINE 2 TEXT 'indented' TEXT ' world' LINE 0 > TEXT 'goodbye' == OUTPUT == hello __indented world goodbye ^
Test inférieur avec l'un de nos exemples d'expressions Doc ci-dessus :
AsmInst = str | int
ce qui est exactement ce à quoi nous nous attendons.
Maintenant, passons à la dernière étape : compiler. Cette fonction transforme les expressions IR en instructions ASM, en tenant compte de la largeur de ligne maximale cible :
def interpret(insts: list[AsmInst]) -> str: """Interpret the ASM instructions as a string.""" result = "" for inst in insts: match inst: case text if isinstance(text, str): result += inst case indent if isinstance(indent, int): result += f"\n{' ' * indent}" return result
Voici l'idée approximative de l'algorithme :
['onions', ['carrots', 'celery'], 'turnips']
le processus est l'endroit où la magie opère :
group( '[' + nest( 4, br() + "'onions'" + ',' + br(' ') + group( '[' + nest(4, br() + "'carrots'" + ',' + br(' ') + "'celery'") + br() + ']' ) + ',' + br(' ') + "'turnips'" ) + br() + ']' )
En bref :
Comment fonctionnefits_flat ? Il parcourt simplement les instructions du groupe, traite les brs comme du texte et s'arrête quand :
TEXT '[' LINE 4 TEXT "'onions'" TEXT ',' LINE 4 TEXT '[' TEXT '' TEXT "'carrots'" TEXT ',' TEXT ' ' TEXT "'celery'" TEXT '' TEXT ']' TEXT ',' LINE 4 TEXT "'turnips'" LINE 0 TEXT ']'
Nous pouvons enfin assembler les pièces :
[ 'onions', ['carrots', 'celery'], 'turnips' ]
Les seuls éléments restants de l'interface de Pretty-Printer sont les constructeurs d'expressions de documents :
[ 'onions', [ 'carrots', 'celery' ], 'turnips' ]
Essayons l'exemple de l'introduction :
['onions', ['carrots', 'celery'], 'turnips']
Voir la source complète ici.
? En construction ?
? En construction ?
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!