JavaScript Interop with Reason and BuckleScript

June
3,
2017
·
tutorial,
ocaml,
reason,
javascript

So you’re all ready to write some Reason but you need to call a JavaScript function? Or maybe you can’t figure out how to write something in OCaml-land and wish you could just bail for a minute & write it in JavaScript? Fortunately, both of those are fairly easy to pull off.

If you don’t already have Reason + BuckleScript set up on your machine, head over to the “Getting Started” blog post or clone this github repository for a minimal boilerplate. If you want a primer on Reason syntax, the Reason documentation provides a nice comparison to JavaScript.

I’ll be using Reason syntax instead of standard OCaml syntax because I like it much better :) but all of the stuff here applies to vanilla OCaml + Bucklescript as well, and the syntax is quite similar. The latest version of Bucklescript as of this writing is 1.7.4, so if yours is later than that, some details might be different. In all of this, you can consult the excellent Bucklescript documentation.

Just dumping JavaScript in the middle of your Reason code

If you’re just hacking things together, this can be very nice, but you also have all of the unsafety of JavaScript code 😄.

Js.log "this is reason";
[%%bs.raw {|
console.log('here is some javascript for you');
|}];

{| and |} are the delimeters of a multi-line string in OCaml. You can also put a tag in there e.g. {something| and then it will look for a matching |something} to close.

And here’s the resulting javascript:

// Generated by BUCKLESCRIPT VERSION 1.7.4, PLEASE EDIT WITH CARE
'use strict';
console.log("this is reason");
console.log('here is some javascript for you');

Dumping in some JavaScript, and making it accessible from Reason

What if you want a value that can be used from your Reason code?

Js.log "this is reason";
let x = [%bs.raw {| 'here is a string from javascript' |}];
Js.log (x ^ " back in reason land"); /* ^ is the operator for string concat */

Now you might be wondering "what magic is this?? How did ocaml know that x was a string? It doesn’t. The type of x in this code is a magic type that will unify with anything! This is quite dangerous and can have cascading effects in OCaml’s type inference algorithm.

let y = [%bs.raw {| 'something' |}];
Js.log ("a string" ^ y, 10 + y);
/* danger!! ocaml won't stop you from using y as 2 totally different types */

To fix this, you should always provide a concrete type for the result of bs.raw.

let x: string = [%bs.raw {| 'well-typed' |}];
Js.log (x ^ " back in reason land");
/* ocaml will error out if you try to use x as anything other than a string */

And here’s the output!

// Generated by BUCKLESCRIPT VERSION 1.7.4, PLEASE EDIT WITH CARE
'use strict';
console.log("this is reason");
var x = ( 'here is a string from javascript' );
console.log(x + " back in reason land");
var y = ( 'something' );
console.log(/* tuple */[
      "a string" + y,
      10 + y | 0
    ]);
var x$1 = ( 'well-typed' );
console.log(x$1 + " back in reason land");

The difference between the 2 %% from the previous section and the 1 % here is important! [%%something ...] is an OCaml “extension point” that represents a top-level statement (it can’t show up inside a function or value, for example). [%something ...] is an extension point that stands in for an expression, and can be put just about anywhere – but make sure that the JavaScript you put inside is actually an expression! E.g. don’t put a semicolon after it, or you’ll get a syntax error when you try to run the resulting JavaScript.

Dumping in a function & passing values

We’ll need a little knowledge about Bucklescript’s runtime representation of various values for this to work.

  • strings are strings, ints and floats are just numbers
  • an Array is a mutable fixed-length list in OCaml, and is represented as a plain javascript array.
  • a List is an immutable functional-style linked list, and is definitely the more idiomatic one to use in most cases. However, it’s representation is more complicated (try Js.log [1,2,3,4] to check it out). Because of this, I generally convert to & from Arrays when I’m talking to javascript, via Array.of_list and Array.to_list.
  • If you want to go deeper, there’s an exhaustive list on the BuckleScript wiki

Knowing that, we can write a function in JavaScript that just accepts an array and returns a number, without much trouble at all.

let jsCalculate: array int => int => int = [%bs.raw {|
 function (numbers, scaleFactor) {
   var result = 0;
   numbers.forEach(number => {
     result += number;
   });
   return result * scaleFactor;
 }
|}];
let calculate numbers scaleFactor =>
  jsCalculate (Array.of_list numbers) scaleFactor;
Js.log (calculate [1,2,3] 10); /* -> 60 */

Of course, this function that I wrote in JavaScript could be ported over to Reason without much hassle.

Remember that this is an escape hatch that’s very useful for learning so you can jump in quickly and make something, but it’s a good exercise to go back through and convert things back into nice type safe reason code.

I’ve run into more than a few bugs because of raw JavaScript that I added to save time 😅.

Settling down and getting disciplined about things

So far we’ve been using bs.raw, which is a very fast n loose way to do it, and not suitable for production.

But what if we actually need to call a function that’s in JavaScript? It’s needed for interacting with the DOM, or using node modules. In BuckleScript, you use an external declaration (docs).

Getting a value and getting a function are both pretty easy:

external pi: float = "Math.PI" [@@bs.val];
let tau = pi *. 2.0;
external alert: string => void = "alert" [@@bs.val];
alert "hello";

But what about when we want something more complicated? Here’s how we could call getContext on a Canvas DOM node:

type canvas;
type context;
/* we're leaving these types abstract, because we won't
 * be using them directly anywhere */
external getContext: canvas => string => context = "" [@@bs.send];
let myCanvas: canvas = [%bs.raw {| document.getElementById("mycanvas") |}];
let ctx = getContext myCanvas "2d";

So let’s unpack what’s going on. We created some abstract types for the Canvas DOM node and the associated RenderingContext object.

Then we made a getContext function, but instead of @@bs.val we used @@bs.send, and we used an empty string for the text of the external. @@bs.send means “we’re calling a method on the first argument”, which in this case is the canvas. BuckleScript will translate this into theFirstArgument.getContext(theSecondArgument, ...).

And the empty string means “the js-name is the same as the name we’re giving the external here in BuckleScript-land”, in this case getContext. If we wanted to name it something else (like getRenderingContext), when we’d have to supply the string "getContext" so that BuckleScript calls the right function.

Let’s add one more function just so it’s interesting.

external fillRect: context => float => float => float => float => unit = "" [@@bs.send];

And now we can draw something!

fillRect ctx 0.0 0.0 100.0 100.0;

It’s not much, but adding other canvas methods is similar, and then you can start doing some really fun things.

So what does the compiled JavaScript look like?

'use strict';
var tau = Math.PI * 2.0;
alert("hello");
var myCanvas = ( document.getElementById("mycanvas") );
var ctx = myCanvas.getContext("2d");
ctx.fillRect(0.0, 0.0, 100.0, 100.0);

Wow! Notice how BuckleScript just inlined our pi variable for us? And the output looks almost exactly like it was written by hand.

What’s next?

Join us in our Discord channel!

Check out the reason-react-example repository if you want to make some UIs.

Here are some repositories that make use of externals:

If you’re starting into Reason, keep track of the things that confuse you and let us know! There’s lots of documentation work to do, and it will be best if it’s informed by people who are just starting out.