Utiliser ReasonML avec JavaScript

bloodyowl
bloodyowl 2017/10/10

Dans un précédent article, on vous présentait une nouvelle syntaxe pour OCaml: ReasonML, elle rend le langage plus accessible en le rapprochant de JavaScript moderne.

À l'aide du projet BuckleScript qui supporte Reason nativement, on peut compiler notre code Reason vers JavaScript très facilement. Le créateur de BuckleScript utilisait à l'origine un autre projet appelé js_of_ocaml. Trouvant qu'il serait possible d'optimiser et de rendre plus lisible le code JavaScript en sortie si le compiler commençait son travail à une étape plus haut niveau (une représentation du programme contenant des informations supplémentaires, alors que js_of_ocaml utilise du bytecode), il propose ce changement à la team js_of_ocaml qui refuse, et décide donc de se lancer dans le projet qui deviendra BuckleScript.

Pour démarrer un projet avec BuckleScript et Reason, on ouvre son terminal, et c'est parti:

On installe BuckleScript:

$ npm install -g bs-platform

On initialise le projet:

$ bsb -init my-app -theme basic-reason

Hop, votre projet est prêt dans my-app.

Maintenant la question est: comment est-ce que je peux utiliser du JS dans Reason et vice-versa ?

Les FFI (ou Foreign Function Interface)

OCaml gère naturellement les FFI, notamment pour appeler des fonctions C lorsqu'il compile vers du code natif.

BuckleScript vient les overloader pour les adapter à JavaScript.

Créons une FFI pour la fonction alert :

[@bs.val] external alert : string => unit = "";

On définit:

  • une fonction externe nommée alert
  • qui prend une string et ne retourne rien (ici représenté par la valeur unit)
  • qui est une valeur à simplement récupérer ([@bs.val])

Si on regarde le code JavaScript en sortie, c'est vide. En effet, external est un moyen de définir comment accéder à une valeur ainsi que son type. Si en revanche on utilise la function alert dans le module:

[@bs.val] external alert : string => unit = "";

alert("Hello!");

On voit dans l'output que BuckleScript a inliné l'appel de alert, il n'a pas crée de représentation intermédiaire.

// Generated by BUCKLESCRIPT VERSION 1.9.1, PLEASE EDIT WITH CARE
"use strict";

alert("Hello!");

/*  Not a pure module */

Maintenant amusons nous à créer des bindings pour jQuery, juste pour le fun:

/* On crée un type opaque pour représenter un objet jQuery */
type jQuery;

/* On type le module jQuery */
[@bs.module] external jQuery : string => jQuery = "jquery";

/* On type la méthod `on`, BuckleScript peut naturellement typer
  le pattern de chaining, assez commun en JS, à l'aide de l'annotation
  `bs.send.pipe: type` */
[@bs.send.pipe: jQuery] external on : string => (Dom.event => unit) => jQuery = "";

jQuery(".selector")
  |> on("click", (_) => alert("hey"));

Ce qui va nous sortir:

// Generated by BUCKLESCRIPT VERSION 1.9.1, PLEASE EDIT WITH CARE
"use strict";

var JQuery = require("jquery");

JQuery(".selector").on("click", function() {
  alert("hey");
  return /* () */ 0;
});

/*  Not a pure module */

Comme on peut le constater, le code de sortie ressemble beaucoup à ce qu'on pourrait écrire à la main.

Pour en savoir un peu plus sur les FFI JavaScript:

Les objets

On peut directement utiliser des objets JavaScript en Reason. Pour accéder à une propriété, on utilise ##.

myJsObject##property

Ça dépanne, mais au sein de notre code Reason, on préférera bien souvent utiliser des records: ils ont une représentation plus légère et sont par défaut immutables. Pour effectuer une conversion, on procède de la manière suivante:

type jsUser = {
  .
  "id": string,
  "username": string,
  /* valeur pouvant être null, undefined, ou la valeur */
  "birthdate": Js.Null_undefined.t(string),
  /* "light" ou "dark", les enums sont souvent représentés par des strings en JS */
  "theme": string
};

/* En Reason, les enums sont représentés par des variants */
type theme =
  | Light
  | Dark;

type user = {
  id: string,
  username: string,
  /* pas de null ou undefined, on utilise un type option */
  birthdate: option(string),
  theme
};

/* une fonction de transformation JS -> Reason */
let fromJs = (jsUser) => {
  id: jsUser##id,
  username: jsUser##username,
  /* BuckleScript propose des helpers pour les conversions */
  birthdate: Js.Null_undefined.to_opt(jsUser##birthdate),
  theme:
    switch jsUser##theme {
    | "dark" => Dark
    | "light"
    /* On match une chaîne de caractère, le match n'est pas exhaustif,
         on doit donc définir la valeur de fallback (par defaut) à l'aide
         de `_`
       */
    | _ => Light
    }
};

/* Pour créer un objet JS en Reason, il suffit de l'écrire comme un
     record, mais avec des clés entre quotes, comme du JSON.
   */
let toJs = (user) => {
  "id": user.id,
  "username": user.username,
  "birthdate": Js.Null_undefined.from_opt(user.birthdate),
  "theme":
    switch user.theme {
    | Light => "light"
    | Dark => "dark"
    }
};

Le code en sortie:

// Generated by BUCKLESCRIPT VERSION 1.9.1, PLEASE EDIT WITH CARE
"use strict";

var Js_primitive = require("bs-platform/lib/js/js_primitive.js");
var Js_null_undefined = require("bs-platform/lib/js/js_null_undefined.js");

function fromJs(jsUser) {
  var match = jsUser.theme;
  var tmp;
  switch (match) {
    case "dark":
      tmp = /* Dark */ 1;
      break;
    case "light":
      tmp = /* Light */ 0;
      break;
    default:
      tmp = /* Light */ 0;
  }
  return /* record */ [
    /* id */ jsUser.id,
    /* username */ jsUser.username,
    /* birthdate */ Js_primitive.null_undefined_to_opt(jsUser.birthdate),
    /* theme */ tmp,
  ];
}

function toJs(user) {
  var match = user[/* theme */ 3];
  return {
    id: user[/* id */ 0],
    username: user[/* username */ 1],
    birthdate: Js_null_undefined.from_opt(user[/* birthdate */ 2]),
    theme: match !== 0 ? "dark" : "light",
  };
}

exports.fromJs = fromJs;
exports.toJs = toJs;
/* No side effect */

Les standard-libs

Si le besoin s'en fait sentir, BuckleScript propose naturellement la stdlib de JavaScript.

let myArray = [|1, 2, 3, 4, 5|];

myArray
  |> Js.Array.map((item) => item * 2)
  |> Js.Array.reduce((acc, item) => acc + item, 0);

vous sortira:

// Generated by BUCKLESCRIPT VERSION 1.9.1, PLEASE EDIT WITH CARE
"use strict";

var myArray = /* array */ [1, 2, 3, 4, 5];

myArray
  .map(function(item) {
    return item << 1;
  })
  .reduce(function(acc, item) {
    return (acc + item) | 0;
  }, 0);

exports.myArray = myArray;
/*  Not a pure module */

La technique bourrin

Pour les cas extrêmes ou vous voulez juste balancer une fonction JS, vous pouvez:

/* %bs.raw pour une expression, %%bs.raw pour un bloc de code arbitraire */
let log: string => unit = [%bs.raw {|
  function (a) {
    console.log(a);
  }
|}];

log("ok");

qui vous sort un joli:

// Generated by BUCKLESCRIPT VERSION 1.9.1, PLEASE EDIT WITH CARE
"use strict";

var Curry = require("bs-platform/lib/js/curry.js");

var log = function(a) {
  console.log(a);
};

Curry._1(log, "ok");

exports.log = log;
/* log Not a pure module */

Conclusion

OCaml peut en grande majorité être compilé en JavaScript très simple, parce qu'il partage beaucoup de concepts avec ce dernier. Il est assez simple de créer des ponts entre les deux langages à l'aide des FFI. L'énorme avantage de cette feature, c'est que l'on peut commencer à utiliser Reason incrémentalement sur son projet sans avoir à tout réécrire d'un coup.

Bisous bisous.

Vous avez aimé cet article?
Le partager sur Twitter
← Articles
Ne rien rater
Sur les réseaux
Twitter
Facebook
Facebook
Apple Podcast
Soundcloud
Sur le chat
Discord