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 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:
// 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');
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.
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 numbersJs.log [1,2,3,4]
to check it out). Because of this, I generally convert to & from Array
s when I'm talking to javascript, via Array.of_list
and Array.to_list
.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 😅.
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:
external pi: float = "Math.PI" [@@bs.val];
let tau = pi *. 2.0;
external alert: string => unit = "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. Given the above, BuckleScript will translate getContext theFirstArgument theSecondArgument
into theFirstArgument.getContext(theSecondArgument, ...)
.
The empty string means "the JS name is the same as the name we're giving the external in BuckleScript-land" – in this case getContext
. If we wanted to name it something else (like getRenderingContext
), then 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.
When folks write bindings 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 bindings, 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 bindings. 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
bindings (converted to Reason, the original is OCaml):
type error;
external glob : string => (Js.nullable error => array string => unit) => unit = "" [@@bs.module];
external sync : string => array string = "" [@@bs.val] [@@bs.module "glob"];
And the relevant parts of package.json
:
{
"name": "bs-glob",
"version": "0.1.0",
...
"devDependencies": {
"bs-platform": "^1.9.1"
},
"dependencies": {
"glob": "^7.1.2"
}
}