DEV Community

Yawar Amin
Yawar Amin

Posted on • Edited on

How does BuckleScript's JavaScript interop work?

BUCKLESCRIPT has made a name for itself as a fast OCaml-JavaScript transpiler with good support for JavaScript interop. But how should you as a beginner get a handle on how it works?

The first thing to do is to bookmark the Interop section of the BuckleScript docs. The second thing to be aware of is that these docs don't cover everything that the BuckleScript interop enables you to do. For a more detailed reference, I recommend bookmarking the following:

Finally, an invaluable tool to bookmark is the 'Try Reason' online playground, where you can quickly experiment with interop code: http://reasonml.github.io/en/try.html

Key ideas

To better understand the references listed above, it's helpful to have a mental model of what the BuckleScript FFI code does. Here's the key idea: BuckleScript bindings (interop declarations) are specifications that the BuckleScript compiler mechanically translates into output JavaScript. Thus each binding needs to contain enough information to convert OCaml/Reason code at call sites into output JavaScript. Over time, you'll learn what information goes where in each binding to produce the correct JavaScript. This can be slightly tricky but it helps tremendously to experiment and watch the JavaScript output (which updates almost instantaneously if there's no compile error).

Here's the second key idea: a binding is just a declaration; it doesn't generate any output JavaScript by itself. The output is generated only at the call site, or the place where the binding is used. This means for example that you can distribute a bindings package of pure OCaml/Reason code and the generated JavaScript will be produced in its consuming package.

The third thing to remember that BuckleScript's interop is pretty powerful, which invariably means that there's usually more than one way to produce the JavaScript you need. Sometimes it's a matter of perspective as to which one is more correct; sometimes a matter of thinking through the semantics (the meaning of the JavaScript you wish to output), and sometimes just a matter of experience.

Example

Here's a simple example that demonstrates the above ideas. I want to call document.getElementById("main"). How to go about doing that?

I'll break down the problem into parts. The first part is how to get document.getElementById. As you may know, document is always in scope inside a browser. And getElementById is always supported by the document object. So, in this case we can use the [@bs.scope] and [@bs.val] extensions: https://bucklescript.github.io/docs/en/bind-to-global-values#global-modules

type element;

[@bs.scope "document"] [@bs.val]
external getElementById: string => Js.nullable(element) = "";

/* This triggers the output: */
let main = getElementById("main");
Enter fullscreen mode Exit fullscreen mode

Output:

var main = document.getElementById("main");
Enter fullscreen mode Exit fullscreen mode

Here's how the mechanical translation is happening, in terms of the output JS:

BuckleScript/Reason outputs JavaScript
let main = var main =
[@bs.scope "document"] document.
[@bs.val] external getElementById getElementById(
"main" "main"

This works, but it's a little incorrect because, semantically, [@bs.scope] is meant to be used for JavaScript modules and [@bs.val] for global values in modules.

Example, approach 2

A better way is to model document as a globally-available object with an abstract type and a corresponding method getElementById, which is semantically what they are according to the Web API.

type document;
type element;

[@bs.val] external document: document = "";
[@bs.send.pipe: document] external getElementById: string => Js.nullable(element) = "";

/* This triggers the output: */
let main = getElementById("main", document);
/* Or: let main = document |> getElementById("main"); */
Enter fullscreen mode Exit fullscreen mode

Output:

var main = document.getElementById("main");
Enter fullscreen mode Exit fullscreen mode

This is the exact same output but now I'm modelling the types and values differently in the Reason codebase. I explicitly have a document type that's declared to accept a method call getElementById. Here's the mechanical translation for this approach, again from the JavaScript perspective:

BuckleScript/Reason outputs JavaScript
let main = var main =
[@bs.send.pipe: document], [@bs.val] external document: document = "";, getElementById(..., document) document.
external getElementById getElementById(
"main" "main"

Here I'm using the [@bs.send.pipe: document] extension to declare that the document type supports a method named getElementById and takes a string parameter. BuckleScript interprets this on the OCaml/Reason side as a function that can be called with a string and a document, but generates output JS that calls the appropriate method on the document with a string argument.

Conclusion

BuckleScript works by using bindings to convert idiomatic OCaml input (modules, types, functions, and values) into simplified JavaScript output (values, functions, method calls etc.). These bindings are declarations that capture all the information that's needed to do the conversion, in a mechanical way. By trying out different bindings and observing the resulting JavaScript output, you can gain a lot of intuition on how to go about writing bindings that are idiomatic and semantically correct.

Top comments (0)