BUCKLESCRIPT ships with a convenient feature that helps you call JavaScript functions more safely: constraining parameter values to certain strings. Suppose you have a JavaScript function (e.g. a React component):
// src/rbtree.js
/** @param node a node.
@param color can be either 'red' or 'black'.
@return the painted node. */
function paint(node, color) { ... }
In BuckleScript you can model this function like so:
// src/Rbtree.re
type node;
[@bs.module "./rbtree"] external paint: (
node,
[@bs.string] [`red | `black],
) => node = "";
The special type:
[@bs.string] [`red | `black]
...tells BuckleScript to accept exactly the following values:
`red
`black
...but convert them into their string representations for the output JavaScript. (These values are called polymorphic variants, by the way.) This way, you control exactly what strings will ultimately be output and guarantee the JavaScript function is called correctly. This is covered in the documentation: https://bucklescript.github.io/docs/en/function#constrain-arguments-better
The reuse problem
The problem with the above technique is that the special type:
[@bs.string] [`red | `black]
...can't be captured as a named type and reused–not even when you leave out the annotation. It's a special syntactic construct that must be typed in literally, every time it's used.
But, multiple functions in a library you're using might want exactly the same set of strings as an input parameter. So you would need to repeat this boilerplate for all of them. For bigger libraries this repetition can get quite cumbersome (and error-prone).
The 'private type' solution
You can instead define a private type for the color parameter:
module Color: {
type t = pri string;
let red: t;
let black: t;
} = {
type t = string;
let red = "red";
let black = "black";
};
[@bs.module "./rbtree"] external paint: (node, Color.t) => node = "";
The type Color.t
is defined as a 'private string', meaning you can't create values of Color.t
but you can convert them into string
s. And, it really is string
under the hood. Now users can pass in only the defined values (enforced by the compiler): Color.red
or Color.black
. And the output values in JavaScript will be exactly 'red'
and 'black'
. And the best part is the Color.t
is a real type, not a syntax construct, and can be passed around and used across functions.
There is only one small issue with this solution: you are introducing a little bit of extra runtime into your library binding by allocating all the allowed values upfront, and including them in your binding library. When every byte of JavaScript that you ship to the browser matters, it can be quite helpful to be able to minimize that.
The 'inline' technique
Fortunately, BuckleScript recently shipped with the ability to explicitly inline values: https://bucklescript.github.io/blog/2019/04/09/release-schedule
Note that I say 'explicitly' because BuckleScript has always been rather great at inlining ('constant folding') any values that it can calculate at compile time. E.g. try compiling this: let test = 2 + 2;
. You'll get the output var test = 4;
. And this is only a simple example.
So, with the explicit inlining feature you can actually mark your color values as inlined:
module Color: {
type t = pri string;
[@bs.inline "red"] let red: t;
[@bs.inline "black"] let black: t;
} = {
type t = string;
[@bs.inline] let red = "red";
[@bs.inline] let black = "black";
};
This tells BuckleScript to inline the usages of Color.red
and Color.black
, with the given literal values. If you're wondering if the repetition is really necessary–it is, but it does act as a self-check, so all in all not bad.
Now, the extra runtime introduced by this binding is almost nil. You should expect to see an empty 'module' Color
in the JavaScript output–that's it. You pay the cost of those values at runtime only when you use them.
By the way, this [@bs.inline]
attribute supports values of type string
, int
, and bool
only. So it is limited but it should cover quite a lot of the JavaScript literals that you would typically pass in to functions.
Top comments (6)
It’s very inspiring and enlightening to see how BuckleScript interop evolves. I mean, it’s one thing to come up with
type color = "red" | "green";
in TypeScript that is tailor-made for JS, but marrying age-old OCaml semantics with JavaScript reality is a whole other story.Thank you for this recipe!
BTW, I started reading your book, and it’s really good so far.
Thank you! I hope you enjoy it.
Thanks for this article! I am using this technique to binding enumerations in the @pixi/constants package (here: github.com/pixijs/pixi.js/blob/dev...).
instead of
Do you think this would be the correct approach?
Hi, no problem. Yes the first code sample looks correct to me. If you add the
[@bs.inline]
attribute in the correct places as explained in the post, the compiler will generate constant literals at the right places.How can I pattern match over type Color.t?
So, there are two possible answers here. The first and direct answer is, you can do something like
The main trick in this code snippet is the part
(color :> string)
which upcasts thecolor
value (of typeColor.t
) into astring
. That's what the private type enables–it lets the type be upcast into its supertype, in this casestring
.The second answer here is that, in this example
Color.t
values are not really meant to be consumed from the Reason side. ThisColor
module is really a helper for JS interop. It's meant to safely send exact string values into JavaScript functions. If you need to pattern-match i.e. consume these values that indicates there's something else going on and maybe a different approach is required. You can kind of tell because of the catch-all pattern_
at the end of theswitch
–you're switching on a 'lower level' value like a string so you don't have a limited set of cases like a variant.