Inlined values in BuckleScript

Yawar Amin - Dec 11 '19 - - Dev Community

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) { ... }
Enter fullscreen mode Exit fullscreen mode

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 = "";
Enter fullscreen mode Exit fullscreen mode

The special type:

[@bs.string] [`red | `black]
Enter fullscreen mode Exit fullscreen mode

...tells BuckleScript to accept exactly the following values:

`red
`black
Enter fullscreen mode Exit fullscreen mode

...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]
Enter fullscreen mode Exit fullscreen mode

...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 = "";
Enter fullscreen mode Exit fullscreen mode

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 strings. 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";
};
Enter fullscreen mode Exit fullscreen mode

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .