Exploring Reason's (Bucklescript's) Interop with JavaScript

3ddabd219208d1fbc6f5b003144ca673?s=47 David Gomes
February 01, 2018

Exploring Reason's (Bucklescript's) Interop with JavaScript

In this talk, we explore Bucklescript's Interop System. While the Bucklescript documentation describes the various operators and some of the design decisions, this talk will follow an example-oriented approach where we write bindings for a JavaScript API and create a small Reason demo that leverages this JS API. In the end, we'll also take a look at how the reverse operation can be performed — consuming a Reason library from JavaScript.

3ddabd219208d1fbc6f5b003144ca673?s=128

David Gomes

February 01, 2018
Tweet

Transcript

  1. 1.

    Exploring Reason's Interop with JavaScript (and by "Reason" I mean

    "Bucklescript") ReasonML Munich Meetup, February 1st, 2018 1
  2. 4.

    How do I use JavaScript from Reason? [%%raw "var a

    = 5"]; let add = [%raw {| function(b) { console.log("hello from raw JavaScript!"); return a + b } |}]; Js.log(add(2)); (* 7 *) 4
  3. 5.

    [%%raw "var a = 5"]; let add = [%raw {|

    function(b) { console.log("hello from raw JavaScript!"); return a + b } |}]; Js.log(add(2)); (* 7 *) Js.log(add + 8); (* ????????????? *) 5
  4. 6.

    let add: ((int, int) => int) = [%raw {| function(a,

    b) { console.log("hello from raw JavaScript!"); return a + b } |}]; Js.log(add(5, 6)); 6
  5. 7.

    How can we make this even better? More declarative, more

    reproducible, easier to reason about 7
  6. 9.

    What operators are there? We are going to skip most

    of them Source: https://bucklescript.github.io/docs/en/interop-cheatsheet.html 9
  7. 10.

    Operator Use [@bs.new] new Instance() [@bs.val] variables/functions [@bs.send] obj.method() [@bs.obj]

    creating JavaScript objects [@bs.splice] functions with unknown number of homogenous args … … 10
  8. 15.

    type dt; [@bs.module "luxon"] [@bs.scope "DateTime"] [@bs.val] external local :

    unit => dt = ""; let myDate = local(); (* DateTime { ts: 2018-01-11T22:51:14.520+00:00, zone: Europe/Lisbon, locale: und } *) 15
  9. 16.

    // Generated by BUCKLESCRIPT VERSION 2.1.0, PLEASE EDIT WITH CARE

    'use strict'; var Luxon = require("luxon"); var myDate = Luxon.DateTime.local(); console.log(myDate); exports.myDate = myDate; /* myDate Not a pure module */ 16
  10. 18.

    (* The type of setZone is: (string, dt) => dt

    *) let myDate = local() |> setZone("America/New_York"); 18
  11. 19.

    19

  12. 20.

    [@bs.send.pipe : dt] external minusMs : int => dt =

    "minus"; [@bs.send.pipe : dt] external minus : duration => dt = "minus"; [@bs.send.pipe : dt] external minusObj : durationObj => dt = "minus"; 20
  13. 21.

    We could also use polymorphic variant types… [@bs.send.pipe : dt]

    external minus : ([@bs.unwrap] [ | `Duration(duration) | `DurationObj(durationObj) | `Int(int) ]) => dt = ""; (see github.com/davidgomes/bs-luxon to see how) 21
  14. 22.

    type durationObj = { . "weeks": int, "months": int, };

    [@bs.obj] external makeDurObj : ( ~weeks: int=?, ~months: int=?, unit ) => durationObj = ""; [@bs.send.pipe : dt] external minusObj : durationObj => dt = "minus"; 22
  15. 23.

    let myDate = local() |> setZone("America/New_York") |> minusObj(makeDurObj(~weeks=1, ())); Note

    that here, one should hide the calling of makeDurObj by shadowing it using this technique. 23
  16. 24.

    [@bs.send.pipe : dt] external endOf : ( [@bs.string] [ |

    `year | `month | `week | `day | `hour | …] ) => dt = ""; 24
  17. 25.

    DateTime .local() .setZone("America/New_York") .minus({ weeks: 1 }) .endOf("day"); let myDate

    = local() |> setZone("America/New_York") |> minusObj(makeDurObj(~weeks=1, ())) |> endOf(`day); 25
  18. 27.

    var mysql = require("mysql"); var connection = mysql.createConnection({ host: "127.0.0.1",

    user: "root" }); connection.query("SHOW DATABASES", function (error, results, fields) { if (error) { console.log(error); return; } console.log(results, fields); }); connection.end(); (source: github.com/mysqljs/mysql) 27
  19. 28.

    let conn = Mysql.createConnection( ~host="127.0.0.1", ~port=3306, ~user="root", () ); Mysql.query(conn,

    "SHOW DATABASES", (error, results, fields) => switch (Js.Nullable.to_opt(error)) { | None => Js.log(results); Js.log(fields); | Some(error) => Js.log(error##message) } ); Mysql.endConnection(conn); 28
  20. 29.

    let conn = Mysql.createConnection( ~host="127.0.0.1", ~port=3306, ~user="root", () ); Mysql.query(conn,

    "SHOW DATABASES", result => { switch (result) { | Ok(results) => Js.log(results.results) | Error(err) => Js.log(err##message) } }); Mysql.endConnection(conn); 29
  21. 33.

    // Generated by BUCKLESCRIPT VERSION 2.1.0, PLEASE EDIT WITH CARE

    'use strict'; function isAre(n) { var match = +(n === 1); if (match !== 0) { return "is"; } else { return "are"; } } exports.isAre = isAre; /* No side effect */ 33
  22. 34.

    const { isAre } = require("./demo.bs"); const word = "meetups";

    console.log(`${word} ${isAre(word, 3)} fun`); // => meetups are fun 34
  23. 36.

    let requiredFieldValidator = value => if (String.length(value) == 0) {

    Js.Option.some("Field is required."); } else { None; }; switch (requiredFieldValidator("test")) { | None => Js.log("all good") | Some(err) => Js.log("error: " ++ err) } 36
  24. 38.

    Internal Representations for Reason Types Operator Use int number string

    string tuple array list [] -> 0 [1] -> 1 [1, 2] -> [1, [2, 0]] option('a) None -> 0 Some(a) -> [a] - … 38
  25. 40.

    Reason let requiredFieldValidatorJs = value => Js.Null.from_opt(requiredFieldValidator(value)); JavaScript const {

    requiredFieldValidatorJs } = require("./demo.bs"); console.log(requiredFieldValidatorJs("test")); // => null 40
  26. 42.

    type person = { age: int, name: string }; let

    personToString = (p: person) => { let name = p.name; let age = p.age; {j|$(name) is $(age) years old|j}; }; let david = { age: 22, name: "David Gomes" }; Js.log(personToString(david)); (* => "David Gomes is 22 years old" *) 42
  27. 43.

    DO NOT DO THIS const { personToString } = require("./demo.bs");

    console.log(personToString([22, "david"])); 43
  28. 44.

    [@bs.deriving jsConverter] type person = { age: int, name: string

    }; const { personFromJs, personToString } = require("./demo.bs"); console.log(personToString(personFromJs({ name: "David", age: 22, }))); 44
  29. 48.

    3 Bindings don't need to be 1-1 mappings of the

    original API (and probably shouldn't be) 48
  30. 49.

    4 You can (and should) write test for bindings test("DateTime.local(2017)

    is the beginning of the year", () => { let dt = DateTime.local(~year=2017, ()); expect(getDateObject(dt)) |> toEqual({ "year": 2017, "month": 1, "day": 1, "hour": 0, "minute": 0, "second": 0, "millisecond": 0 }); }); 49