Interop
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 😄.
REJs.log("this is reason");
[%bs.raw {| console.log('here is some javascript for you') |}];
{|
and|}
are the delimiters 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:
JS// 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?
REJs.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.
RElet 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
.
RElet 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!
JS// 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
andfloats
are just numbersan 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, its representation is more complicated (try
Js.log([1,2,3,4])
to check it out). Because of this, I generally convert to & fromArray
s when I'm talking to javascript, viaArray.of_list
andArray.to_list
.If you want to go deeper, there's a list in the BuckleScript documentation
Knowing that, we can write a function in JavaScript that just accepts an array and returns a number, without much trouble at all.
RElet 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 and 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:
RE[@bs.val] external pi : float = "Math.PI";
let tau = pi *. 2.0;
[@bs.val] external alert : string => unit = "alert";
alert("hello");
But what about when we want something more complicated? Here's how we could call getContext
on a Canvas DOM node:
REtype canvas;
type context;
/* we're leaving these types abstract, because we won't
* be using them directly anywhere */
[@bs.send] external getContext : (canvas, string) => context = "getContext";
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
. @bs.send
means "we're calling a method on the first argument", which in this case is the canvas. Given the above, BuckleScript will translate getContext(theFirstArgument, theSecondArgument)
into theFirstArgument.getContext(theSecondArgument, ...)
.
Let's add one more function just so it's interesting.
RE[@bs.send] external fillRect : (context, float, float, float, float) => unit = "fillRect";
And now we can draw something!
REfillRect(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?
JS'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.
Using existing JavaScript libraries
When folks write wrappers for a particular JavaScript library, they'd usually publish it to npm. Head over to the Libraries to find out how to find these.
To use a library that does not have existing wrappers, however, you'll want to first install the npm package as usual, e.g. using npm install --save <package-name>
, then just go ahead and write your wrapper. You'll probably find the bs.module
FFI feature particularly useful; it emits the right import
s or require
s, depending on the JS compilation target's module format.
As an example, here's the entire source code of the bs.glob
wrapper (converted to Reason, the original is OCaml):
REtype error;
[@bs.module] external glob : (string, (Js.nullable(error), array(string)) => unit) => unit = "glob";
[@bs.val] [@bs.module "glob"] external sync : string => array(string) = "glob";
And the relevant parts of package.json
:
JSON{ "name": "bs-glob", "version": "0.1.0", ... "devDependencies": { "bs-platform": "^1.9.1" }, "dependencies": { "glob": "^7.1.2" } }