Weekly Side Project 1 : PEG.js editor

I think I will try to write more about my experiments. Hopefully my english won't hurt anyone ! :)

This time, I wanted to fool around by working on a parser using PEG.js. I knew it well, having used it on some other projects, but creating the grammar was still a pain : the only editor around here is the online one, and is not very user-friendly. Specifically, it does not have syntaxic coloration, does not save grammars, and can hang if you happen to write a bad grammar (which can happen unexpectedly while you are still typing).

To solve this issue, I've worked on an improved IDE which is now done. It uses Angular and CodeMirror for the frontend, and Express and SQLite for the backend. It's main features are :

  • Syntaxic highlighting on both PEG.js grammar files[1] and JSON output
  • Snippets can be easily saved (either ctrl+s or the big Save button) and shared (the page URL will automatically be updated after sucessful save)
  • The generated parsers will hopefully never hang. Try for example this grammar, your browser will detect the infinite loop and timeout the process (Web Workers).

I'm open to any suggestion and bug report, please feel free to post them on the project repository.

Some examples :


[1] Let's thanks Forbes Lindesay for doing the CodeMirror mode a few days before I start this project ! I have made a PEG.js Ace mode a bit before, but I wasn't really fond of Ace due to its undocumented way to add new modes without recompiling the whole thing, so I was really looking forward to using CodeMirror.

Coding Style

Ce blog post se consacre à quelques pratiques que j'ai adopté dans mes développements, et en particulier les plus "borderlines" : celles qui sont sans doute les moins couramment utilisées, ou les plus sujets à débats.

Notez que certains exemples ne suivent pas l'ensemble des conventions de ce document. J'ai essayé de ne pas surcharger les exemples afin de bien mettre en valeur le point évoqué.


Toujours mettre un espace entre les opérateurs, y compris (particularité) entre les parenthèses et autres brackets.

Simplement pour aérer le code, et permettre de séparer rapidement chaque composant à la lecture.

L'humain n'est pas très bon pour lexer manuellement un bloc de code (d'où l'existence de la coloration syntaxique), autant lui simplifier la vie en séparant clairement les débuts et fin des différents éléments.

var makePair = function ( first, second ) {  
  return [ first, second ];
};

Faire un usage intensif des lignes vides pour séparer les blocs sémantiques.

Faire cela permet d'aérer le code, le rendant plus aisé à relire par la suite.

Un conseil pour une découpe correcte : imaginez le pseudo-code de votre fonction, aussi simple qu'elle soit. Chaque ligne de ce pseudo-code peut être transcrit comme un bloc sémantique. Séparez-les par des lignes vides, et la fonction n'en sera que plus lisible et compréhensible.

var inheritor = function ( Base, Source ) {

  var F = function ( ) { };
  F.prototype = Base.prototype;

  Source.prototype = new F( );

  return Source;
};

Si un morceau de code peut être rendu fluide, envisager d'en tirer parti.

Un bloc fluide représente généralement une unique instruction mais, plus important : elle n'est pas censée en contenir d'autres (c'est à dire qu'aucune évolution du code ne devrait à priori la complexifier).

Ce style d'écriture convient donc particulièrement à des fonctions et méthodes helpers, comme dans l'exemple qui suit :

var buildAuthConfiguration = function ( user, pass ) {  
  return { user : user
         , pass : pass }; }

var buildAdditionAST = function ( functionName, a, b ) {  
  return new AST.Addition(
    new AST.Number( a ),
    new AST.Number( b ) ); };

Éviter les closures libres.

Les closures sont un composant important de Javascript, a tel point que les développeurs les découvrant ont une légère tendance à les utiliser partout à la fois. Ce n'est pas une bonne idée : bien que puissantes, elles introduisent une complexité supplémentaire lorsque le code doit être lu : où est définie la fonction ? Quelles variables utilise-t-elle ? Quelles variables viennent du scope global (argh), et quelles autres du scope parent ?

En règle générale, une closure peut assez souvent être remplacée, au moins en grande partie, par une méthode ou fonction dédiée. L'idéal est de ne les utiliser que dans un seul cas : en tant que callback (celles-ci n'introduisant pas d'ambigüité quand à leur champs d'action.


Utiliser la bibliothèque standard, y compris les fonctions apportées par ES5.

Trop de codes Javascript utilisent à mon goût certains 'templates' de code, dont vous pouvez quelques exemples sur ce gist. Bien que certains pourraient arguer que ces codes sont plus optimisés, dans le sens où ils ne nécessitent pas d'"inutiles" appels de fonction, je préfère considérer d'une part que les performances d'un forEach ou d'un map sont rarement le bottleneck d'une application classique, et d'autre part que le moteur d'exécution sait probablement mieux optimiser certains cas que moi.

Utiliser les fonctions standard permet d'apporter un style de développement conventionné : un développeur lisant le code aura certainement plus de facilité à comprendre la façon dont il est censé s'exécuter, et l'état d'esprit de la personne l'ayant écrit.


Se limiter à un composant par fichier.

Je rencontre régulièrement des projets disposant de plusieurs classes / prototypes par fichier. C'est à mon sens une erreur, principalement parce que compartimenter une classe dans un fichier permet de mieux souligner leur indépendance.

Regardez par exemple le tree de Castel. Simple et élégant : vous avez instantanément accès à toutes les informations dont vous pourriez avoir besoin. Vous savez comment sont agencées les classes, vous savez lesquelles sont disponibles. Aucune documentation particulière n'est ainsi requise.

Ce concept se transpose également dans les applications web. A l'heure où nous disposons de systèmes de gestion de dépendances efficaces (Require.js, CommonJS-Everywhere, ...), il est dommage de rester sur des architectures où retrouver du code revient à faire des grep et espérer tomber sur le filon.

Commentaire conditionnel

Aujourd'hui, juste un petit tricks amusant que j'ai découvert il y a pas mal de temps maintenant mais qui trouve régulièrement son utilité pendant le développement ou le débuggage de features.

Vous savez sûrement que dans certains langages de programmation, vous pouvez commenter soit via //, soit /* ... */. Mais saviez-vous qu'il vous est également possible de mixer les deux de la façon suivante ?

/*
    ... whatever A
    return 42;
/*/
    ... whatever B
    return 69;
//*/

L'intérêt de la chose n'est pas évident au premier abord, mais regardez ce qui se passe avec un simple slash en plus :

//*
    ... whatever A
    return 42;
/*/
    ... whatever B
    return 69;
//*/

La branche de code exécutée switche.

Notez que la technique est sympathique, mais est à éviter voire proscrire dans du code de production : avoir deux implémentations différentes d'un bout de code, c'est généralement chercher les ennuis : )

Adieu Backbone, bonjour Angular

Quand vous faites du développement Javascript depuis un certain temps, vous finissez forcément par vous apercevoir que le langage n'est pas forcément simple à utiliser proprement. Rappellez-vous : au début, document.write() était considéré comme le net plus ultra de la génération dynamique de code. Ca date.

Au fur et à mesure de la prise de conscience générale que le JS, c'est quand même vachement cool, un besoin s'est de plus en plus fait sentir : celui d'établir des process clairs en ce qui concerne le développement d'applications enrichies. Des initiatives ont été prises petit à petit, mais se sont pendant longtemps bornées au rôle de bibliothèque : Prototype, Scriptaculous, jQuery, Dojo, ... sont autant de bibliothèques relativement vieilles ayant facilité l'écriture de tâches spécialisées.

Cependant, c'est très récemment que le concept d'application enrichie s'est véritablement démocratisé. Cela a plus ou moins coincidé avec la création de la bibliothèque Backbone. Cette dernière fournissait non pas des fonctionnalités, mais plutôt des process de développement. Elle définissait ce qu'était un modèle, une collection et une vue, permettant aux développeurs sur des bases relativement solides. C'était une première, très peu de bibliothèques allaient dans ce sens à cette époque (on peut citer par exemple Ext.js, mais cette dernière était très spécialisée et n'était pas adaptée aux sites classiques).

Durant les derniers mois (les dernières années, en fait), j'ai pratiqué beaucoup de Backbone. D'abord par effet de mode, puis parce que j'aimais bien ce côté structuré de la bibliothèque : j'aime beaucoup tout ce qui se rapproche à de la conception. Cependant, j'ai toujours trouvé qu'il y avait plusieurs problèmes inhérents à la bibliothèque : la gestion de template était très erratique, le binding (et surtout unbinding) d'évènements était limité, et il n'y avait pas vraiment de consensus sur la façon de faire du bon backbone. En conséquence, il était souvent possible de faire du bon travail avec la bibliothèque, mais également souvent au prix d'efforts de réflexion dès lors que l'on voulait faire quelque chose de propre.

Il y a environ trois mois, j'ai entrepris la création d'un projet personnel dont je parlerais peut-être un jour. Conscient des limites de Backbone que j'ai évoqué ci-dessus, j'ai voulu me tourner vers une des autres bibliothèques qui ont vu le jour à la suite de Backbone. M'étant précédemment essayé à Ember.js, je suis tout d'abord retourné dessus. Autant dire que je n'y suis pas resté longtemps. Un framework qui me parle de machine à état lorsque l'on touche à du routing rate clairement quelque chose dans mon esprit. Bref, je n'ai pas du tout aimé Ember, et m'en suis donc rapidement détourné.

Mon second essai a été avec Angular. Et là, je dois le reconnaitre, je suis assez épaté.

Pour ceux qui ne la connaisse pas, Angular est une bibliothèque créée par les équipes de Google et censée apporter leur réponses à la question : "comment faire une application enrichie en utilisant les deux technologies natives : HTML et Javascript ?". Cette bibliothèque est particulière à plusieurs niveaux :

  • Elle s'intègre directement au sein du document lui-même. Vous n'avez (à ma connaissance) pas de template en Angular. Et s'il en existe, je pense que les use case sont particulièrement bien cachés
  • Elle étend l'HTML en rajoutant des balises logiques telles que if ou forEach
  • Elle gère en interne toute la magie de la mise à jour des éléments au fur et à mesure de la modification des collections
  • Chaque composant peut posséder son propre controlleur, en réalité une simple fonction Javascript

Ce billet ne rentrera pas en détail dans son fonctionnement, pour cela vous devrez le découvrir par vous-même (ou me recruter :). Cependant, sachez que j'ai été véritablement impressionné par la simplicité du fonctionnement d'Angular. La courbe d'apprentissage est très rapide (une seule notion nécessaire : le scope), l'écriture tout aussi rapide (quelques balises nécessaires pour les forEach, quelques lignes pour remplir un tableau JS, Angular se charge du reste), et l'ensemble est partiulièrement scalable. Si vous supprimez un élement d'une collection, vous supprimerez aussi les handlers qui y sont attaché, ne changeant ainsi rien à la fluidité de l'application.

En bref, utilisez Angular. Ne serait-ce que dans un projet personnel. Faites une todo, puis comparez avec le code Backbone qu'il vous aurait fallu. Vous constaterez une nette différence dans la clareté du code, ce qui le rendra plus facilement modifiable lorsque vous voudrez plus tard l'adapter à de nouveaux besoins.

Mon dernier travail en freelance concerne du Backbone. Je ne peux m'empêcher de remarquer qu'utiliser Angular aurait permis de résoudre un très grand nombre de problématiques en un claquement de doigts, et d'éviter un énorme travail 'inutile' pour 'chauffer' l'environnement avant de commencer le code de l'application proprement dit.

Le tort du marteau

Dans le cadre d'un projet que j'ai démarré, et dont je parlerais peut-être plus tard, je dois effectuer de l'ETL : Extract / Transform / Load, afin de transformer une shitload de données XML en un montant bien plus raisonnable de données CSV.

Le fichier XML faisant 13Go, j'ai exclu d'emblée DOM pour partir sur un parseur Sax, beaucoup plus efficace sur les gros sets de données de ce genre (pour information, Sax consiste à parser le flux XML tel qu'il arrive, tandis que DOM reconstruit l'arbre en mémoire).

Je suis parti sur du Node, d'abord parce que j'aime bien le Javascript, ensuite parce que j'ai l'habitude de bosser avec, et enfin parce qu'un langage de script me permettait d'abstraire beaucoup de problématiques.

Finalement, après avoir réfléchi un peu comment lire correctement du Sax avec une machine a état et tout le bordel, j'ai désespéré, et me suis tourné vers une solution dont on m'avait précédemment parlé : Kettle. Je n'aborderais pas le sujet en détail, mais considérez qu'il s'agit d'un software spécialement conçu pour effectuer des ETL. Malheureusement, il a les deux défauts d'être à la fois GUI et Java-based, donc je l'ai finalement laissé tomber pour retourner sur mon Node.

Plutôt que de partir sur une machine a état comme je le pensais originellement, j'ai réfléchi un peu à la raison pour laquelle j'utilisais Sax : j'avais beaucoup de données. Beaucoup de lignes dans ma table. Et là, ça a été l'évidence : mon arbre XML n'était pas gros en largeur, uniquement en hauteur.

Et je suis donc parti sur l'idée d'utiliser Sax pour construire un arbre XML en mémoire (en utilisant de simples objets Javascript très basiques) puis, une fois qu'un certain 'path' était construit, le transformer en sous-document XML DOM, et utiliser une poignées d'expressions XPath dessus afin de récupérer les informations qui m'intéressaient.

Cette approche a donc été implémentée (en utilisant les deux plugins node sax et jsdom), mais j'ai été confronté à un problème que je n'avais pas anticipé à ce point : la vitesse d'exécution était atroce.

Pour donner un ordre d'idée, j'ai laissé tourner le script de conversion depuis hier soir 19h. Cela fait maintenant 24h, nous en sommes à la 727500eme ligne. Le document en possède plus detrois millions, et j'ai un autre document beaucoup, beaucoup plus gros à traiter derrière.

Autant dire que si je voulais avoir fini avant la fin du monde mois, il allait falloir trouver mieux. J'ai donc essayé quelques optimisations, telles que supprimer la récursion de la fonction vérifiant si l'on avait rencontré un noeud XML 'intéressant', réutiliser le même objet XML à chaque noeud, etc, mais rien n'y faisait, le script restait toujours désespérement lent.

Et puis vint l'idée que j'aurais dû avoir avant : commenter les expressions XPath. Et là, le changement fût vraiment significatif, passant de 5s pour 1000 lignes à moins d'une seconde.

Bien évidemment, j'avais vraiment besoin de ces expressions, pour éviter de devoir écrire beaucoup de code et risquer de tout voir s'effondrer à une possible mise à jour du format d'entrée (toujours être générique ;). Cependant, je n'avais pas vraiment besoin du moteur XPath en lui-même ... il était conçu pour supporter énormément de features, et je n'en utilisait presque aucune. Bref, long story short (ce post commence à s'éterniser, pardon :), j'ai supprimé la conversion Object -> DOM, et ait rajouté une fonction au prototype de mes objets permettant de trouver un sous-noeud. Une autre fonction pour trouver leur contenu textuel, et j'étais paré. Le résultat final est beaucoup plus acceptable : 21 minutes pour traiter les trois millions de lignes ! J'aime node.

En ce qui concerne le titre de ce post, ne cherchez pas, c'est simplement un jeu de mot avec le marteau de Thor. Oui. Pardon.

[note] Pour référence, le fichier de 32 millions de lignes s'est processé en 308 minutes. Je ne sais pas si c'est très respectable, mais étant donné qu'il tourne dans une VM, et que je peux tout de même lancer plusieurs dumps par jour si nécessaire, je suis plutôt satisfait.