Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

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.

David Gomes

February 01, 2018
Tweet

More Decks by David Gomes

Other Decks in Programming

Transcript

  1. Exploring Reason's Interop with
    JavaScript
    (and by "Reason" I mean "Bucklescript")
    ReasonML Munich Meetup, February 1st, 2018
    1

    View Slide

  2. David Gomes
    Software Engineer at MemSQL
    (we are building a very fast database)
    2

    View Slide

  3. Why care about Reason's interop with JavaScript?
    3

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  7. How can we make this even better?
    More declarative, more reproducible, easier to reason about
    7

    View Slide

  8. [@bs.val]
    external sqrt : float => float = "Math.sqrt";
    let four = sqrt(16.0); (* => 4*)
    8

    View Slide

  9. What operators are there?
    We are going to skip most of them
    Source: https://bucklescript.github.io/docs/en/interop-cheatsheet.html
    9

    View Slide

  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

    View Slide

  11. Let's go through a real life use case
    11

    View Slide

  12. Luxon — Example #1
    12

    View Slide

  13. const { DateTime } = require("luxon");
    DateTime
    .local()
    .setZone("America/New_York")
    .minus({ weeks: 1 })
    .endOf("day");
    13

    View Slide

  14. type dt;
    external local : () => dt => "local";
    14

    View Slide

  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

    View Slide

  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

    View Slide

  17. [@bs.send.pipe : dt]
    external setZone : string => dt = "";
    17

    View Slide

  18. (*
    The type of setZone is:
    (string, dt) => dt
    *)
    let myDate =
    local()
    |> setZone("America/New_York");
    18

    View Slide

  19. 19

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  24. [@bs.send.pipe : dt]
    external endOf :
    (
    [@bs.string]
    [ | `year | `month | `week | `day | `hour | …]
    ) =>
    dt = "";
    24

    View Slide

  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

    View Slide

  26. Example #2
    https://bucklescript.github.io/bucklescript/api/Js.html
    The Js Module
    26

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  30. The other way round
    30

    View Slide

  31. A Happy Example
    31

    View Slide

  32. let isAre = n => n == 1 ? "is" : "are";
    32

    View Slide

  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

    View Slide

  34. const { isAre } = require("./demo.bs");
    const word = "meetups";
    console.log(`${word} ${isAre(word, 3)} fun`);
    // => meetups are fun
    34

    View Slide

  35. A different type of example
    35

    View Slide

  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

    View Slide

  37. const { requiredFieldValidator } = require("./demo.bs");
    console.log(requiredFieldValidator("test"));
    // => 0
    37

    View Slide

  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

    View Slide

  39. Warning: Some of these representations might
    change — be careful.
    39

    View Slide

  40. Reason
    let requiredFieldValidatorJs = value =>
    Js.Null.from_opt(requiredFieldValidator(value));
    JavaScript
    const { requiredFieldValidatorJs } = require("./demo.bs");
    console.log(requiredFieldValidatorJs("test"));
    // => null
    40

    View Slide

  41. A third type of example
    41

    View Slide

  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

    View Slide

  43. DO NOT DO THIS
    const { personToString } = require("./demo.bs");
    console.log(personToString([22, "david"]));
    43

    View Slide

  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

    View Slide

  45. Key Takeaways
    45

    View Slide

  46. 1
    Think twice before you start to bind
    46

    View Slide

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

    View Slide

  48. 3
    Bindings don't need to be 1-1 mappings of the original API (and
    probably shouldn't be)
    48

    View Slide

  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

    View Slide

  50. 5
    You should use interface (.rei/.mli) files
    50

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide