Vers un langage spécifique pour la description de séries chronologiques réalistes

À l’origine: TSimulus

Il y a de cela plusieurs années, mon équipe était confrontée à un problème récurrent: nous développions des outils pour gérer des données issues de séries chronologiques, mais ne disposions pas de suffisamment de telles données pour valider notre travail.

Par « série chronologique », nous entendons ici une séquence ordonnée de moments dans le temps, et à chaque moment est associée une valeur. Par exemple, le relevé horaire de la température par une station météo constitue une série chronologique.

Afin de solutionner notre problème, j’ai conçu un DSL permettant d’exprimer des séries chronologiques « aléatoires mais réalistes ». Il s’agissait de définir des blocs de bases permettant de spécifier l’évolution de valeurs scalaires au cours du temps, puis de combiner ces blocs afin d’en créer de plus complexes. Par exemple, il était possible de définir les valeurs d’une série chronologique comme la somme des valeurs de deux autres séries. Ou encore, de spécifier qu’une série chronologique aurait initialement pour valeurs celles d’une série déjà décrite, mais qu’à partir d’un certain moment, une transition se produirait pour que, progressivement, les valeurs d’une autre série soient utilisées.

J’ai ainsi défini quelques dizaines de descripteurs de bases, des combinateurs, des comparateurs, des transformateurs et des filtres afin d’exprimer des séries chronologiques de plus en plus complexes. Simultanément, des collègues et moi-même avons implémenté des parseurs pour ce DSL, ainsi que des bibliothèques, des CLI, des services Web exploitant ce DSL pour générer les valeurs des séries chronologiques décrites. Le DSL, ainsi que l’ensemble des outils le supportant, étaient regroupés sous le nom de projet TSimulus.

Chaque bloc combinable était décrit sous la forme d’un objet JSON. Cela a plusieurs avantages. Il existe déjà des parseurs pour JSON que nous pouvions réutiliser et spécialiser. Il est également possible de spécifier des schémas décrivant la structure que devaient respecter les descriptions de séries chronologiques, afin que nous-mêmes ou les utilisateurs de nos outils puissent valider ces descriptions de manière systématique et fiable. JSON est également un langage de choix dans le monde des services Web: si vous voulez proposer un service quelconque sur le Web, on s’attendra généralement à ce qu’il reçoive en argument des objets JSON et produise en retour d’autres objets JSON.

De par son ouverture, ce format est également adapté à la conception de DSL évolutifs: lorsque nous inventions une nouvelle brique de base, il suffisait d’en décrire la représentation sous la forme d’un objet JSON. Nos outils pouvaient alors prendre en compte des descriptions de séries chronologiques intégrant cette nouvelle brique, sans devoir réviser fondamentalement nos structures descriptives.

Cette approche a bien fonctionné. Lorsque le projet était open source, nous avons même eu des utilisateurs dont nous ne soupçonnions pas l’existence. Néanmoins, elle souffre d’un grand défaut: si écrire des objets JSON complexes n’est pas un problème pour une application outillée de manière appropriée, il n’en est pas de même pour des êtres humains. Pour eux, ce format est alambiqué, verbeux, non intuitif et, si son formalisme a un côté rassurant, l’utiliser en pratique provoque rapidement quelques gouttes de sueur sur le front du rédacteur le plus consciencieux.

De JSON à un langage réellement spécifique

J’en ai déduit que le DSL proposé n’était pas le plus adapté pour la description de séries chronologiques par un être humain. A posteriori, je rejoins l’avis de Martin Fowler concernant la peur du parseur, et en particulier en ce qui concerne les causes de cette peur:


Alors pourquoi existe-t-il une peur irrationnelle à écrire des parseurs? Je crois que ça se résume à deux raisons principales.
– Vous n’avez pas suivi de cours de compilation à l’université et pensez donc que les parseurs sont effrayants.
– Vous avez suivi un cours de compilation à l’université et pensez donc que les parseurs sont effrayants.

Martin Fowler

J’ai remarqué que cette peur avait souvent un impact important au niveau décisionnel: pourquoi risquer de voir un développeur passer des jours à concevoir un DSL, à le tester et à le documenter, alors qu’on pourrait avoir la certitude de le voir passer des jours à concevoir des structures en JSON, à les tester et à les documenter?

Mais, que la perspective d’écrire un parseur pour un DSL externe vous effraie ou non, je pense que la difficulté que l’exercice représente doit s’effacer devant la plus-value qu’il apporte aux utilisateurs. Un DSL, par définition, est destiné à être utilisé par une personne du métier, qui n’est a priori pas informaticienne, et pour qui la syntaxe de JSON (ou XML ou autre) sera perçue au mieux comme une source de distraction, au pire comme des signes cabalistiques qu’il faudra bien apprendre pour faire son travail.

Peut-être un langage plus concis, tel que YAML, trouvera-t-il plus facilement grâce à ses yeux. Mais, dans notre problématique, cette concision ne résout pas le problème principal de JSON, à savoir qu’il s’agit d’appliquer de manière spécifique un langage généraliste. Ce qui implique nécessairement un bruit qui n’est pas souhaitable.

Ébauche de langage

Dans une nouvelle approche, chaque série chronologique serait décrite sur une ligne de texte, ou bien sur plusieurs si un symbole de fin de description était utilisé. Cela ressemblerait fort à l’affectation d’une variable telle qu’elle s’écrit dans de nombreux langages de programmation. Une différence notable avec ceux-ci est que le langage serait purement descriptif, la manière de convertir les descriptions en une exécution restant à charge d’un programme tiers.

L’utilisation d’un descripteur de base ressemblerait à l’invocation d’une fonction:

a = uniform(0,1)

Équivalent en JSON:

{
  "a": {
    "type": "uniform",
    "min": 0,
    "max": 1
  }
}

On note la disparition d’un attribut générique type au profit d’une fonction ayant des arguments spécifiques et typés. Cela facilite l’utilisation de fonctionnalités telles que la coloration syntaxique, voire la détection d’erreurs syntaxiques et sémantiques, offertes par un éditeur supportant notre langage.

La combinaison de descripteurs pourrait se faire en utilisant des opérateurs qui ont du sens pour l’utilisateur:

a = b + c

plutôt qu’en utilisant une structure JSON ad hoc:

{
  "a": {
    "type": "sum",
    "terms": ["b", "c"]
  }
}

Lorsqu’un tel opérateur n’est pas disponible, une fonction peut à nouveau être utilisée:

a = max(b, c, 123.456)

plutôt que

{
  "a": {
    "type": "max",
    "parameters": ["b", "c", 123.456]
  }
}

Mise en œuvre

La mise en œuvre d’un langage implique traditionnellement le recours à une série d’outils éprouvés et passablement rébarbatifs, tels que Lex, Yacc et/ou Bison, ainsi qu’à un langage permettant de décrire la grammaire du langage. Je voudrais ici considérer deux alternatives.

La première, que je n’ai pas encore eu l’occasion d’expérimenter, consister à utiliser MPS, une plate-forme proposée par Jetbrains pour la création de DSL.

La seconde n’est destinée qu’aux développeurs Scala. Ce langage disposait initialement d’un parseur intégré à sa bibliothèque standard, mais il est à présent proposé sous la forme d’une bibliothèque autonome. Parser Combinators doit son nom au fait que les parseurs créés sont combinables (composables): de « petits » parseurs, décrivant des éléments bien définis, peuvent être utilisés conjointement pour produire des parseurs plus ambitieux.

On utilise cette bibliothèque en créant en Scala une case class par concept présent dans le langage à supporter: dans notre cas, tous les descripteurs de base, toutes les combinaisons, toutes les transformations, et plus généralement tout ce qu’il est possible de décrire est formalisé par une classe dont les instances sont immuables.

On crée alors des parseurs, qui sont essentiellement des fonctions faisant office de règles. Ces règles associent à un certain schéma une fonction permettant de créer une instance de case class représentant un concept. Lors de la lecture d’un flux de caractères, les parseurs vont tenter d’y repérer les schémas qui ont été définis, et, lorsque c’est le cas, vont appliquer la fonction associée au schéma afin de convertir l’élément détecté en une instance de classe représentant cet élément.

Il résulte de l’application des parseurs une représentation des éléments présents dans le flux analysé: typiquement, un arbre syntaxique abstrait dont les nœuds sont de simples et purs objets Scala.

Il reste alors à exploiter cet arbre en décidant des actions à entreprendre lors de son parcours. Les fonctionnalités de pattern matching de Scala s’avèrent très pratiques pour cette exploitation.

La principale spécificité de Parser Combinators, par rapport aux approches plus traditionnelles, est à la fois une force et une faiblesse: cette bibliothèque permet la création de parseurs en pur Scala, et leur exploitation produit des objets Scala tout à fait ordinaires. Cette forte intégration réduit fortement, voire annule, le surcoût de création de parseurs: il ne s’agit que d’exploiter une nouvelle bibliothèque. Les parseurs générés n’étant que des objets Scala, il n’est pas possible d’utiliser les outils supportant les langages de grammaires plus généralistes: on se prive de l’écosystème logiciel généralement associé à la création de DSL.

Conclusion

Mes occupations professionnelles actuelles m’empêchent de consacrer le temps nécessaire à l’évolution de TSimulus, et en particulier de son DSL permettant la description de séries chronologiques. Je suis néanmoins persuadé que la mise à disposition d’un langage textuel, proche dans sa syntaxe de Java ou de Python, mais spécialisé dans ce type de description, serait un plus pour l’utilisateur final.

J’ai proposé deux approches pour créer un tel langage. Elles s’éloignent de celle, traditionnelle, consistant à exploiter une chaîne d’outils allant progressivement de la grammaire formellement définie dans un fichier texte à un code source prêt à être intégré au sein d’un programme. Je vous laisse juger de l’intérêt de considérer ces approches la prochaine fois que vous serez confrontés à un problème d’évolution similaire au mien.

Laisser un commentaire