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. Exploring Reason's Interop with JavaScript (and by "Reason" I mean

    "Bucklescript") ReasonML Munich Meetup, February 1st, 2018 1
  2. David Gomes Software Engineer at MemSQL (we are building a

    very fast database) 2
  3. Why care about Reason's interop with JavaScript? 3

  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
  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
  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
  7. How can we make this even better? More declarative, more

    reproducible, easier to reason about 7
  8. [@bs.val] external sqrt : float => float = "Math.sqrt"; let

    four = sqrt(16.0); (* => 4*) 8
  9. What operators are there? We are going to skip most

    of them Source: https://bucklescript.github.io/docs/en/interop-cheatsheet.html 9
  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
  11. Let's go through a real life use case 11

  12. Luxon — Example #1 12

  13. const { DateTime } = require("luxon"); DateTime .local() .setZone("America/New_York") .minus({

    weeks: 1 }) .endOf("day"); 13
  14. type dt; external local : () => dt => "local";

    14
  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
  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
  17. [@bs.send.pipe : dt] external setZone : string => dt =

    ""; 17
  18. (* The type of setZone is: (string, dt) => dt

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

  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
  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
  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
  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
  24. [@bs.send.pipe : dt] external endOf : ( [@bs.string] [ |

    `year | `month | `week | `day | `hour | …] ) => dt = ""; 24
  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
  26. Example #2 https://bucklescript.github.io/bucklescript/api/Js.html The Js Module 26

  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
  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
  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
  30. The other way round 30

  31. A Happy Example 31

  32. let isAre = n => n == 1 ? "is"

    : "are"; 32
  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
  34. const { isAre } = require("./demo.bs"); const word = "meetups";

    console.log(`${word} ${isAre(word, 3)} fun`); // => meetups are fun 34
  35. A different type of example 35

  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
  37. const { requiredFieldValidator } = require("./demo.bs"); console.log(requiredFieldValidator("test")); // => 0

    37
  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
  39. Warning: Some of these representations might change — be careful.

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

    requiredFieldValidatorJs } = require("./demo.bs"); console.log(requiredFieldValidatorJs("test")); // => null 40
  41. A third type of example 41

  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
  43. DO NOT DO THIS const { personToString } = require("./demo.bs");

    console.log(personToString([22, "david"])); 43
  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
  45. Key Takeaways 45

  46. 1 Think twice before you start to bind 46

  47. 2 Gradually reach full type safety, don't be afraid to

    start slowly 47
  48. 3 Bindings don't need to be 1-1 mappings of the

    original API (and probably shouldn't be) 48
  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
  50. 5 You should use interface (.rei/.mli) files 50

  51. 6 Inspect your JS output every now and then 51

  52. 7 Writing bindings is a great way of learning Reason/OCaml

    52
  53. 8 Having JavaScript clients consume your Reason libraries might require

    some work. 53
  54. Thank you! @davidrfgomes on Twitter github.com/davidgomes 54