Introduction à Opa

Publié le 31 octobre 2012 par Nicolas Zermati | back

Cet article est publié sous licence CC BY-NC-SA

Dans le monde du développement web, langages et frameworks se multiplient. Parmi ces derniers, lesquels relèvent du bricolage et lesquels s’avèrent être de véritables innovations ? Aujourd’hui je voudrai faire une petite introduction au langage Opa, qui, selon moi, apporte véritablement de la nouveauté.

Opa se définit lui même comme un framework pour Javascript, mais il s’agit en réalité d’un langage de programmation à part entière. Le compilateur Opa produit du code Javascript pour NodeJS, la syntaxe du langage a été calquée sur le Javascript et la librairie standard se concentre sur le web. Je suppose que le choix de l’appellation « framework » est tendance et moins effrayante.

Un programme Opa permet de générer le Javascript client, le CSS, le HTML, les interactions vers les bases de données ainsi que le back-end d’une application.

Typage

Le point fort d’Opa est le fait de disposer d’un système de type évolué. On peut prendre l’exemple de la fonction greeting de l’exemple suivant :

// Define a person record containing only a name
person = { name: "Joe" }

// Define a greeting function that create our XHTML message
greeting = function(person) { Xhtml.of_string("Hello , {person.name}!") }

// Create a web server that returns the greeting
Server.start(Server.http,
  { title: "Greet me"
  , page: function() { greeting(person) }
  }
)

Ce code génère un serveur web complet. La page affichée contient le texte « Hello, Joe! ».

Les avantages apportés par le système de type sont multiples. Prenons le cas où la fonction greeting est modifiée :

greeting = function(person) {
  Xhtml.of_string("Hello , {person.firstname} {person.lastname}!")
}

alors on aura, lors de la compilation, le message d’erreur suivant :

Error: File "/home/n25/tmp/article.opa", line 12, characters 22-41, (12:22-12:41 | 354-373)
Type Conflict
  (2:10-2:24)         { name: string } / 'c.a
  (6:29-6:44)         { firstname: 'a; lastname: 'b; 'r.a }

  The argument of function greeting should be of type
    { firstname: 'a; lastname: 'b; 'r.a }
  instead of
    { name: string } / 'c.a

Ce message indique qu’à la ligne 12 se trouve l’erreur greeting(person). Les types de l’enregistrement person et de l’argument de la fonction greeting, respectivement lignes 2 et 6, sont incompatibles.

On corrige donc person de la sorte :

person = { lastname: "Smith", firstname: "Joe" }

Avant même de lancer l’application on sait que notre refactoring de greeting n’est pas cohérent. En javascript ce code aurait très bien fonctionné et affiché : « Hello , undefined undefined! ». Cet exemple est simpliste. Le compilateur est pourtant capable de pointer toutes les incohérences de ce genre quelque soit la complexité du code.

Maintenant, encore plus évident, on fait une simple faute de frappe :

parson = { lastname: "Smith", firstname: "Joe" }

Ici, même chose, le compilateur met immédiatement le doigt sur l’erreur. Et qui n’a jamais perdu de temps à rechercher une faute de frappe ?

Error: File "/home/n25/tmp/article.opa", line 12, characters 33-38, (12:33-12:38 | 389-394)
  the variable person is unbound.
Hint:
  Perhaps you meant parson or prerrln ?

Voilà pour cet avant goût de ce qu’apporte le typage. Beaucoup d’autres langages (Haskell, OCaml, Scala, Java, Ada, etc) ont compris les intérets d’un système de type en matière de sécurité. Dans le milieu du web aussi les choses évolues en ce sens. Opa n’est pas le seul à aller dans cette direction ; par exemple TypeScript ou Roy sont deux tentatives d’apporter un système de type à Javascript.

Compilé

Opa se distingue également par le fait qu’il s’agit d’un langage compilé. Cette phase de compilation ralentit le workflow classique offert par des langages interprétés tels que Ruby. Cependant, bien conscient de ce point noir, des outils de compilation à la sauvegarde existent (voir opa-watch et l’article associé).

Cette phase de compilation apporte non seulement les vérifications de type, mais également l’opportunité d’optimiser différents aspects du programme.

Fonctionnel

Opa est un langage fonctionnel. C’est un gros changement par rapport à Ruby qui même s’il utilise beaucoup d’idées issues du monde fonctionnel reste très orienté objet.

Cependant, il faut bien admettre que le fonctionnel à la cote ces dernières années. En effet, la programmation fonctionnelle apporte modularité, abstraction, peu ou pas d’effets de bord à un programme. La maintenance et l’évolution en sont facilitées.

Mélanger le fond et forme

Opa dispose de nombreux types de données pour déclarer du style CSS. Voici, par exemple, la notation d’un type Css.properties :

style = css { background: blue; }

Dans le cas général il semble plus clair d’utiliser des feuilles de styles externes, servies en tant que ressources.

Opa dispose d’un type de donnée et d’une syntaxe dédiée pour représenter un morceau de XHTML :

xhtml_person = <div class="person">{person.firstname} {person.lastname}</>

La ligne précédente crée une variable xhtml_person de type xhtml. Par rapport à un morceau de XHTML classique il y a des différences :

  • il n’est pas nécessaire de nommer les balises de fermeture,
  • on peut utiliser la notation comme un template (voir l’exemple),
  • la classe est une liste de chaines de caractères ou bien du texte,
  • le style est un type Css.properties (une liste de propriétés CSS) ou bien du texte,
  • les évènements Javascript sont également typés : (Dom.event) → void et Dom.event_option et
  • le HTML est analysé pour vérifié qu’il n’y a pas de balise non fermée ou mal imbriquée.

Il m’est difficile de me passer d’outils comme SASS. Si on le souhaite, on peut sans problème utiliser un préprocesseur conjointement à Opa, moyennant l’écriture de quelques scripts. Pour HAML, je ne pense pas qu’il soit possible de l’utiliser directement. Étendre la syntaxe d’OPA serait réalisable puisque le projet est open source, avis aux amateurs ;-)

Même si Opa permet d’embarquer du HTML et du CSS dans le code, cela n’empêche pas de séparer le fond de la forme dans l’organisation de son code. Depuis peu les dernières versions d’Opa intègrent même un générateur de projets suivant l’architecture MVC : « Opa-create ».

Importer des librairies

L’écosystème Javascript est riche, il serait dommage de s’en priver. Il est possible d’utiliser des librairies Javascript depuis Opa. Pour cela, il faut écrire un fichier de binding et spécifier les types des paramètres et des valeurs de retour des fonctions disponibles. En plus de ça il est d’usage de définir un package Opa qui va encapsuler par une API le binding Javascript. Pour plus d’information voir la documentation.

C’est une étape assez fastidieuse mais une fois que les bindings sont disponibles pour une bibliothèque, il est facile de les maintenir et des les partager.

Manipuler le DOM

Opa dispose, tout comme l’incoutournable JQuery, de nombreux outils de manipulation du DOM. En voici quelques exemples :

// Ajouter la classe `warning` aux noeuds ayant la class `person`
Dom.add_class(Dom.select_class("person"), "warning")

// Ajouter un élément HTML à la fin du contenu du noeud d'ID `main-container`
Dom.transform([#main-container =+ <span>foo</>])

// Ajouter un élément HTML au début du contenu du noeud d'ID `main-container`
Dom.transform([#main-container += <span>foo</>])

// Remplacer un élément d'ID `main-container` par un autre
Dom.transform([#main-container = <span>foo</>])

// Utiliser un selecteur CSS et itérer sur les résultats
selection = Dom.select_raw("div.person")
iterator  = function(dom) { ... }
Dom.iter(iterator, selection)

Pour plus de détails, voir la documentation du module Dom. On ne retrouve pas, pour le moment, la magie d’un $ ou d’un _ que peuvent offrir les librairies Javascript classiques.

Slicing

Le client et le serveur étant codé dans le même programme, le compilateur Opa séparera le code Javascript à exécuter chez le client. Opa dispose d’un slicer permettant de décider si une fonction est exécutée chez le client, sur le serveur ou bien aux deux endroits. Attention, le slicer ne peut pas séparer une fonction en plusieurs parties.

On peut indiquer manuellement au slicer où une fonction doit s’exécuter. Si on force l’exécution d’une fonction coté serveur alors qu’elle aurait pu être exécutée coté client, le programme fonctionnera mais une communication client serveur sera mise en place.

Par exemple dans le cas suivant :

greeting = function(person) {
  <div class="person" onready={bind}>Hello , {person.firstname} {person.lastname}!</>
}

server bind = function(_) {
  selection = Dom.select_class("person")
  ignore(Dom.bind(selection, {click}, function (_) { Dom.add_class(selection, "red") }))
}

La fonction bind a tout intérêt à se trouver chez le client, mais je choisis de forcer son exécution sur le serveur. Dans ce cas un appel à distance du client vers le server est réalisé lorsque l’évènement ready est levé. Le programme fonctionne normalement, la fonction bind n’est pas accessible au client, le DOM est bien modifié.

Communication client serveur transparente

Comme on vient de le voir dans la section précédente, la communication client serveur est automatique et transparente. Voilà un exemple qui en fait usage volontairement :

// Define a person record containing only a name
person = { lastname: "Smith", firstname: "Joe" }

// Define a greeting function that create our XHTML message
greeting = function(person) {
  <div class="person" onready={bind}>Hello , {person.firstname} {person.lastname}!</>
}

server server_operation = function(_) {
  println("Click!")
}

client bind = function(_) {
  selection = Dom.select_class("person")
  ignore(Dom.bind(selection, {click}, server_operation))
}

// Create a web server that returns the greeting
Server.start(Server.http,
  { title: "Greet me"
  , page: function() { greeting(person) }
  }
)

La fonction server_operation est exécutée lorsque le client clique sur un élément person.

Base de donnée

Opa prend en charge les interactions avec la base de donnée. Une couche de typage est rajoutée à la base de donnée ce qui rajoute encore un niveau de sécurité. Le revers de cet avantage est que pour déclarer la base ainsi que pour y accéder, il faut passer par le langage.

Pour le moment la base de donnée de référence est MongoDB. D’autres adapters feront leur apparition dans le futur afin de supporter les bases relationnelles.

Voilà une déclaration d’une collection :

type movie = { int id, string name, int length }

database db {
  movie /movies[{id}]
}

On déclare une base de donnée db contenant une collection movies, qui contient des valeurs de type movie. Ici, le chemin /movies est arbitrairement choisi. Par contre, [{id}] indique que la collection contient plusieurs movie et que la clé primaire de ces derniers est id.

On accède à cette collection de la manière suivante :

// Inserer un film d'id 1
/db/movies[id == 1] <- { name: "Foo", length: 105 }

// Recupérer dans une variable 'm' de type 'movie' le film d'id 1
movie m = /db/movies[id == 1]

// Récupérer dans une variable 'long_movies' l'ensemble des films dépassant 2 heures
dbset(movie, _) long_movies = /db/movies[length > 120]

// Modifier la durée du film d'id égal à 1
/db/movies[id == 1]/length <- 112
/db/movies[id == 1] <- { length: 112 }

// Ajouter 10 minutes à la durée de tout les films
/db/movies <- {length += 10}

Cette approche par chemin peut faire penser au URIs.

Conclusion

Opa tente d’apporter des outils jusque là peu utilisés dans le domaine du developpement web. Grâce à son système de type, et au degré d’abstraction de son API, je pense que c’est véritablement un pas un avant.

Opa n’est pas encore très mature. Il y a des lacunes en ce qui concerne la documentation. Les premiers essais sont difficiles, il faut apprivoiser le compilateur et le langage. Mais ce qu’approrte Opa vaut la peine de franchir ces barrières.

Opa n’est pas LA réponse. Il existe d’autres frameworks, comme Play!, écrit en Scala et inspiré de Ruby on Rails, qui ont les mêmes objectifs.

L’équipe Synbioz.

Libres d’être ensemble.