DEV Community

loading...

Inlined values in BuckleScript

Yawar Amin
Programming languages enthusiast. Author of Learn Type Driven Development: https://www.packtpub.com/application-development/learn-type-driven-development
Updated on ・3 min read

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 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";
};

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.

Discussion (6)

Collapse
hoichi profile image
Sergey Samokhov

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.

Collapse
yawaramin profile image
Yawar Amin Author

Thank you! I hope you enjoy it.

Collapse
sukantpal profile image
Sukant Pal • Edited

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...).

module TYPES: {
    type t = pri int;

    let half_float: t;
} = {
    type t = int;

    let half_float = 36193;
}

instead of

module TYPES = {
  [@bs.scope "TYPES"] [@bs.module "@pixi/constants"]
  external half_float = 36193;
}

Do you think this would be the correct approach?

Collapse
yawaramin profile image
Yawar Amin Author

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.

Collapse
acsreedharreddy profile image
A C SREEDHAR REDDY

How can I pattern match over type Color.t?

Collapse
yawaramin profile image
Yawar Amin Author

So, there are two possible answers here. The first and direct answer is, you can do something like

let test = color =>
  Js.log(switch (color :> string) {
    | "red" => "It's red!"
    | "black" => "It's black!"
    | _ => "I don't know what it is!"
  });

The main trick in this code snippet is the part (color :> string) which upcasts the color value (of type Color.t) into a string. That's what the private type enables–it lets the type be upcast into its supertype, in this case string.

The second answer here is that, in this example Color.t values are not really meant to be consumed from the Reason side. This Color 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 the switch–you're switching on a 'lower level' value like a string so you don't have a limited set of cases like a variant.