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:
- The original BuckleScript manual, in HTML form. It's slightly outdated but the 'OCaml calling JS' section covers some things that the docs site doesn't. https://raw.githubusercontent.com/BuckleScript/bucklescript/8a13b34fdba9f0b4d53f5433b66879df167c4217/docs/Manual.html
- glennsl's excellent cheatsheet gives an overview of most of the interop codes and their JavaScript outputs: https://github.com/glennsl/bucklescript-ffi-cheatsheet
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");
Output:
var main = document.getElementById("main");
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"); */
Output:
var main = document.getElementById("main");
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)