Pegjs: Implémenter un moyen plus simple d'exprimer des listes avec un séparateur

Créé le 21 sept. 2012  ·  25Commentaires  ·  Source: pegjs/pegjs

Lorsque nous avons une récursivité à droite, nous devons faire quelque chose comme ceci :

Statements
  = head:Statement tail:(__ Statement)* {
      var result = [head];
      for (var i = 0; i < tail.length; i++) {
        result.push(tail[i][1]);
      }
      return result;
    }

Je pourrais trouver utile de pouvoir faire la même chose comme suit:

Statements
  = st:Statement (__ st:Statement)* { return st; /*where st is an array*/ }
feature

Tous les 25 commentaires

Connexe: # 69

Il ne s'agit pas vraiment d'une aide à la récursivité droite, mais de listes d'éléments séparés par un séparateur. Il s'agit d'un modèle assez courant dans les grammaires linguistiques et, en tant que tel, il mérite probablement une simplification. La question est de savoir comment le faire.

Je n'aime pas la solution proposée dans la description du problème. Ce n'est que de la magie noire et n'est pas conforme aux règles de portée de l'étiquette telles qu'elles sont décrites au #69. Au lieu de cela, je réfléchis actuellement aux deux solutions suivantes:

Syntaxe spéciale

Imaginez quelque chose comme ça :

Args = args:Arg % ","

La signification serait "une liste de Arg s séparés par "," s. La variable args contiendrait un tableau de tout ce que Arg produit, les séparateurs se faire oublier.

Une question est de savoir comment distinguer les listes séparées autorisant zéro ou plusieurs éléments de celles autorisant un ou plusieurs éléments. L'expérience montre que les deux sont nécessaires. Une réponse possible est d'ajouter ou d'ajouter * ou + à l'opérateur % :

Args0 = args:Arg %* ","
Args1 = args:Arg %+ ","
Args0 = args:Arg *% ","
Args1 = args:Arg +% ","

L'opérateur % pourrait même être implémenté comme modificateur des opérateurs * et + existants :

Args0 = args:Arg* % ","
Args1 = args:Arg+ % ","

Avantages : Simple à mettre en œuvre, n'introduit aucun nouveau concept.
Inconvénients : Solution spécifique à un problème spécifique, pas générique.

Règles paramétriques

La deuxième solution est d'utiliser des règles paramétriques, déjà proposées dans #45. Mon idée actuelle sur la syntaxe:

// Template definition
List<item, separator> = head:item tail:(separator item)* { ... boilerplate ... }

// Template use
Args = List<Arg, ",">

De cette façon, le code passe-partout serait répété au plus une fois dans la grammaire. Le problème avec deux types de listes peut être résolu par deux modèles. Ces modèles pourraient même être intégrés.

Avantages : Générique, peut également éliminer d'autres types de passe-partout.
Inconvénients : Complexe à mettre en œuvre, introduit un nouveau concept.


Je n'ai pas encore décidé quelle direction prendre. J'aimerais entendre des pensées/suggestions/propositions alternatives.

La définition de modèle semble être la voie à suivre pour cela, surtout s'il peut y avoir des modèles intégrés (facultatifs), comme la liste.

J'ai un projet frère à peg.js (otac0n/pegasus, c'est essentiellement un port pour C #) qui utilise déjà des crochets angulaires dans cette position pour le type de données de la règle, mais il semble que je pourrais comprendre quelque chose si vous y alliez avec ça.

Juste mes deux cents:

Syntaxe spéciale

Cette syntaxe doit également être capable de distinguer les listes autorisant deux éléments ou plus. C'est un modèle assez courant que lorsque la liste contient deux éléments ou plus, vous souhaitez l'envelopper dans un nœud de conteneur, alors qu'elle n'a qu'un seul élément, vous renvoyez simplement cet élément.

De plus, le séparateur peut ne pas toujours être supprimé :

complexSelector = simpleSelectors:simpleSelector % (ws* [>+~] ws* / ws+)

Ceci est un exemple de sélecteur CSS, dans lequel vous voulez probablement savoir quels combinateurs sont utilisés.

Règles paramétriques

J'aime cette idée, mais il semble que ce que propose le template soit encore assez limité : seules les expressions peuvent être paramétrées. Si vous souhaitez uniquement échanger * avec + , vous devez utiliser un modèle différent. Vous pouvez bien sûr imbriquer des modèles comme celui-ci :

AbstractList<head, tail> = head:head tail:tail { tail.unshift(head); return tail;  }
List<item, separator> = AbstractList<item, (separator item)*>
List2<item, separator> = AbstractList<item, (separator item)+>

mais:

  • nommer les modèles est difficile

    • le nom List2 en dit peu sur sa caractéristique, et avec sa définition abstraite, la situation est aggravée

    • vous ne voulez certainement pas utiliser ListWithTwoOrMoreItems

  • est-ce vraiment mieux que :

{ var list = function(head, tail) { tail.unshift(head); return tail; } } args = head:arg tail:(',' a:arg {return a})* { return list(head, tail) } args2 = head:arg tail:(',' a:arg {return a})+ { return list(head, tail) }

Personnellement, je trouve cela plus explicite et donc plus lisible.

  • même si les expressions peuvent être paramétrées, l'action est susceptible de supposer qu'elles ont certaines propriétés. Par exemple, si head est un tableau, AbstractList échouera. Il est donc probable que même si les expressions d'un modèle correspondent à la règle actuelle, vous ne l'utiliserez pas réellement.

Le vrai problème

Ce problème concerne vraiment le fait que les utilisateurs souhaitent que pegjs fusionne les valeurs des expressions pour eux, afin qu'ils n'aient pas à le faire manuellement dans les actions.

Je me demande si les étiquettes sont réutilisées comme ça, est-ce qu'il est évident que les utilisateurs veulent que certaines valeurs soient fusionnées ?

args = args:arg args:(',' a:arg {return a})* { // args is an array of "arg"s }
args2 = args:arg args:((',' / ';') a:arg)* { //args is an array of "arg"s and separators "," or ";"

Notez que la deuxième règle aplatit la deuxième valeur de args

La première règle implique que pegjs doit tester les types de valeurs

items = items:item1 items:item2

Si ni item1 ni item2 n'est un tableau, items est [item1, item2] , sinon items est la concaténation des deux.

La deuxième règle, cependant, implique un comportement étrange qui pourrait devoir être modifié.

items = items:item1 items:item2

Si l'un des item1 et item2 est un tableau de tableaux, il doit être aplati, mais reste tel quel lorsque son étiquette est unique

items = items:item1 other:item2

Mais comme lorsque les utilisateurs utilisent une étiquette pour deux expressions, leur esprit a probablement déjà activé le "mode de fusion", cela peut donc être moins déroutant qu'il n'y paraît.

curvemark, je ne suis pas d'accord avec vos inconvénients à puces aux règles paramétriques. Il semble que vos deux premiers points découlent de l'idée que la modularité et l'abstraction rendent en quelque sorte les choses plus difficiles à lire ou à comprendre. C'est clairement faux comme l'ont montré 50 ans d'informatique. Les abstractions sont la racine de tout pouvoir en programmation

Si vous avez des problèmes pour nommer les variables, ce n'est pas la faute du langage. S'attendre à ce que quelqu'un lise l'intérieur de votre règle pour comprendre ce que font vos variables mal nommées n'est pas une solution, c'est un gâchis. Je dirais que vous voulez certainement des noms comme "ListWithTwoOrMoreItems" - il documente votre code, ce qui signifie que vous n'avez pas alors à écrire un commentaire indiquant ce que "List2" signifie.

"est-ce vraiment mieux que ..." - oui, c'est beaucoup plus propre, plus facile à lire et plus facile à entretenir. La situation devient encore plus claire avec des règles encore _légèrement_ plus compliquées

David, je ne comprends pas la nécessité d'une syntaxe spéciale ici. Ce:
Args = args:Arg % ","
pourrait être fait comme ceci:
Args = args:(Arg ",")* { return args.map(function(v){v[0]}) }

Map (et bien sûr réduire également) est une fonction super utile que j'ai utilisée à plusieurs endroits lors de l'utilisation de PEG.js . Oui, c'est un peu plus long que la syntaxe spéciale, mais A. C'est beaucoup plus flexible, et B. ne demande à personne d'apprendre la nouvelle syntaxe PEG. Il n'est certainement pas nécessaire de créer des boucles courtes et laides pour un comportement comme celui-ci.

Il semble que vos deux premiers points découlent de l'idée que la modularité et l'abstraction rendent en quelque sorte les choses plus difficiles à lire ou à comprendre.

non, ce n'est pas ce que je voulais dire. PEG contient déjà un fantastique mécanisme d'abstraction appelé règles

    = number operator number

dans ce cas, number et operator sont des abstractions, et j'adore ça.

La syntaxe du modèle, quant à elle, tente d'abstraire cette règle en paramétrant des expressions. Mais rappelez-vous, PEG est un langage déclaratif, la partie grammaire n'a aucune connaissance de la nature des expressions et la partie grammaire n'autorise aucune syntaxe conditionnelle. Paramétrer signifie simplement remplacer ici. Comparé aux règles, cela n'abstrait presque rien.

Si le but est de réutiliser la structure de la règle, autant écrire une règle plus générale et l'échouer dans l'action.

La situation devient encore plus claire avec des règles encore un peu plus compliquées

Pourriez-vous fournir quelques cas d'utilisation réels où la syntaxe du modèle pourrait être utile, à l'exception des listes avec des séparateurs ou des chaînes avec des guillemets différents, qui traitent essentiellement de la fusion d'expressions et devraient avoir une syntaxe plus ciblée ?

David, je ne comprends pas la nécessité d'une syntaxe spéciale ici.

Args = args:(Arg ",")* est différent de Args = args:Arg % "," . Le premier permet à la règle de se terminer par , , le second non.

Paramétrer signifie simplement remplacer ici.

Ne pourriez-vous pas dire la même chose pour les fonctions dans n'importe quel langage de programmation ? Je suis sûr que nous convenons tous les deux que les fonctions sont utiles, alors pourquoi pas dans un analyseur ?

Pourriez-vous fournir quelques cas d'utilisation réels

J'ai déjà écrit quelques exemples dans le problème principal ici : https://github.com/dmajda/pegjs/issues/45

Le premier permet à la règle de se terminer par ,

Ah je vois, tu as raison. Quoi qu'il en soit, mon argument est toujours que cela pourrait être fait comme ceci:
Args = first:Arg rest:("," Arg)* { return [first].concat(rest.map(function(v){v[0]})) }

La bonne chose à propos des fonctions est que si vous le faites souvent, vous pouvez créer une fonction pour cela et la réduire à :
Args = first:Arg rest:("," Arg)* { return yourFancyFunction(first,rest) }

Et si vous aviez des modèles de règles, vous pourriez être encore plus simple : Args = list<Arg,",">

Il semble que vous parliez d'une fonctionnalité totalement différente.

La syntaxe proposée par David est d'utiliser les paramètres dans la partie grammaire, et comme je l'ai dit, cela remplace simplement les choses. Vous ne pouvez pas tester sa valeur, vous ne pouvez pas spécifier différentes structures de grammaire pour différentes valeurs de paramètres.

Ce que vous suggérez, c'est de les utiliser dans l'action (si je comprends bien), mais je ne sais pas comment cela fonctionne. Le premier exemple de "compte" au #45 ne dit pas d'où vient la valeur initiale de count , ou vous supposez simplement que tous les paramètres ont une valeur par défaut 0 ?

Tu devrais peut-être ouvrir un nouveau sujet pour ça.

@dmajda , je pense à une autre syntaxe pour résoudre le problème de fusion, qui se comporte un peu comme la syntaxe $ expression .

La syntaxe $ expression renvoie la chaîne correspondante, quelle que soit la structure de l'expression. De même, que diriez-vous d'introduire une syntaxe, disons # expression (ou quelque chose de ce genre), qui renvoie un tableau de sous-expressions correspondantes, quelle que soit la structure de l'expression :

args = #(arg (',' a:arg {return a})*) // args is [arg, arg...]

args = #(arg (',' arg)*) // args is [arg, ',', arg, ',', ...]

Cependant, si vous l'écrivez de cette façon

args = #(arg restArgs) // args is [arg, [',', arg, ',', arg, ...]]

restArgs = #(',' arg)* // restArgs is [',', arg, ',', arg, ...]

Ne se comporte pas exactement comme $ expression

@curvedmark , il m'a mentionné dans un e-mail que sa proposition ici correspondait à ce que ma proposition était comprise. Mais peut-être avez-vous raison. Quoi qu'il en soit, ma proposition n'est pas "complètement différente" - c'est une généralisation de ce que vous pensez qu'il propose. Peut-être que @dmajda peut se clarifier. Dois-je ouvrir un nouveau sujet pour ça, David ?

Vous aviez raison à propos de mon exemple de comptage. J'ai mis à jour mon commentaire pour avoir (espérons-le) une structure correcte. Merci.

Pour ce que ça vaut, je serais ravi de l'une ou l'autre des deux suggestions de David. Les règles paramétriques sont extrêmement puissantes et seraient utiles pour beaucoup de choses, donc je pense que je pencherais vers cette solution. Bien que je sois d'accord avec @otac0n en principe sur le fait qu'il serait bien d'avoir des abstractions intégrées ou même que les gens puissent partager des abstractions de manière modulaire, je le ferais simplement et je commencerais simplement par la fonction d'abstraction. Vous pouvez résoudre ces problèmes supplémentaires à l'avenir. Le simple fait de fournir une abstraction de modèle serait une nette amélioration de la concision et éliminerait la duplication de code.

Regexp::Grammars de Perl le fait également avec l'opérateur modulus :

# a list of one or more "item", separated by "separator", returning an array:
<rule: list>
        <[item]>+ % <separator>

C'est en quelque sorte une utilisation discutable du module. Le module en virgule flottante définit un groupe de quotients, un intervalle semi-ouvert de réels (IEEE 754 flotte vraiment). C'est une analogie bâclée puisque les éléments renvoyés par le module Regexp::Grammar ne sont identiques que jusqu'à un modèle (et plus important encore puisque les chaînes ne sont pas un groupe) mais c'est assez proche.

Au lieu d'en faire un opérateur intégré, je viens de créer des analyseurs qui pourraient être paramétrés par des analyseurs.
Métagrammaire ici.

@futagoza Existe-t-il un ticket pour les analyseurs paramétrés ? Je pense qu'il y en avait un, mais je ne l'ai pas trouvé.

Le plus lié que j'ai pu trouver est # 45 mais l'OP de ce problème propose une syntaxe différente (similaire à ce que vous avez implémenté dans votre métagrammaire @polkovnikov-ph), mais je prévois d'utiliser des règles paramétriques (comme @dmajda suggéré ci-dessus) qui utilisent la syntaxe la plus courante de < .. > (modèles, génériques, etc.).

@futagoza La syntaxe n'a pas vraiment d'importance. Quoi que vous finissiez par faire avec ce problème, je l'apprécie grandement.

Le choix du symbole compte beaucoup, car cela peut interférer avec d'autres fonctionnalités hautement prioritaires, comme le patch de portée de @Mingun

Il n'y a pas d'alternative particulièrement sensée pour les gammes, donc mon inclination serait de conserver les accolades d'angle pour ceux

Pour être honnête, je suis une sorte de noob analyseur. Ma solution à cela semble simple et je suis un peu inquiet que cela ne fonctionne pas à grande échelle, mais pourquoi ne pas faire quelque chose comme

fname = "bob" / "dan" / "charlie"
namelist = (WS? fname ","?)+

S'il y a un besoin réel ici, je le soutiens - les listes sont parmi les choses les plus courantes qu'un analyseur doit faire, et il semble qu'il y ait probablement un besoin réel ici, car plusieurs utilisateurs forts sont dans ce fil et n'ont pas dit cela

Donc, ma solution n'est probablement pas assez bonne, mais j'aimerais savoir pourquoi

Donc, ma solution n'est probablement pas assez bonne, mais j'aimerais savoir pourquoi

Principalement parce qu'il accepte certaines entrées, cela dans la vraie vie doit être inacceptable. Par exemple, il analyse bobdan .

Équitable. C'était naïf de ma part.

Cela autorise bob , dan et bob,dan , mais pas bobdan . Qu'est-ce que j'ai raté ici ?

Document = names
WS = [ \r\n]+

fname = "bob" / "dan" / "charlie"

nameitem = (WS? fname ",")+ 
namelastitem = (WS? fname)

namelist = nameitem? namelastitem

@polkovnikov-ph - il y a aussi le #36, qui est similaire au #45 mais pas identique

L'explication de l'auteur semble suggérer que 36 est plus proche de ce que vous vouliez dire

Maintenant, il n'analyse que ce qui est attendu, mais les résultats ne sont pas un seul tableau. C'est la principale motivation pour avoir une construction spéciale pour analyser des données délimitées

D'accord, cela commence à ressembler à quelque chose qui devrait vraiment avoir un opérateur, comme une chose facile à utiliser. La plupart des utilisateurs n'auront pas d'expert russe à qui poser des questions stupides 😁

Qu'en est-il de

Document = names

WS = [ \r\n]+

fname = "bob" / "dan" / "charlie"

namelist = nl:(namelast ",")+ { return nl[0][0]; }
namelast = WS? fn:fname       { return fn; }

names = nl:namelist? na:namelast { return [].concat(nl, na); } 

Ou ukrainien ou autre. Je m'excuse : j'ai vu un nom cyrillique. Je ne devrais pas deviner comme ça. Se tromper peut être offensant.

Fonctionne, bien sûr, mais nous abordons ici la question posée dans le titre - une _manière plus simple_. Les données séparées sont utilisées assez souvent pour avoir une syntaxe distincte pour elles. Dans ma pratique, c'est le deuxième endroit pour lequel j'utilise des plages, le premier est des répétitions avec des données variables ( peg = len:number someData|len|; dans ma syntaxe de plage)

Et oui, je viens de Russie. Ne t'inquiète pas, pas d'offense

D'accord, cela commence à ressembler à quelque chose qui devrait vraiment avoir un opérateur, comme une chose facile à utiliser

nous abordons ici la question posée dans le titre du numéro -- d' une manière plus simple .

Ouais, je suis d'accord maintenant. C'est juste que ma première mauvaise tentative avait l'air vraiment simple, et si ce n'était pas faux, cela aurait été assez simple pour ne pas déranger

mais maintenant que je vois ce qu'il faut, je suis d'accord

Cette page vous a été utile?
0 / 5 - 0 notes