Converting from JS
Preparation
Before you proceed, please make sure that Reason is what your team needs! As much as we vouch for Reason and BuckleScript's popularity, please don't unnecessarily thrash your colleagues and give them a bad first impression. That's hard to undo afterward.
This guide covers a workflow that's helped us convert things over rapidly and efficiently. It's not intended to go over language/FFI features (though it puts them in context). Basic Reason/BuckleScript knowledge is assumed.
Syntax
Goal: first and foremost, make the file syntactically valid. Don't care about wrong types, missing modules, bad file organization, too many externals, etc. We'll come back to clean these up after setting up the regression test that is "no more syntax errors".
Since the Reason syntax resembles enough to that of JavaScript, instead of starting a new Reason file, just copy over an existing js file and work on top of it.
Tip: don't forget that you can use refmt
in your editor/terminal! If you don't know e.g. the precedence of some operations, wrap them in as many parentheses as you wish, then refmt
your code and see which ones remain. Likewise, no need to lose time on indentations and spacing; refmt
takes care of them.
JS/* original JS file you've copied over */
const school = require('school');
const defaultId = 10;
function queryResult(usePayload, payload) {
if (usePayload) {
return payload.student
}
return school.getStudentById(defaultId);
}
Here are some of the things you'd do at this step:
Convert the function call syntax over.
Convert the
var
/const
over tolet
.Hide the
require
s.Make other such changes. For idioms that don't have a BuckleScript equivalent, use
bs.raw
(documentation).
Again, worry only about making the file syntactically valid. Trying to learn all three of syntax, types and other semantics while converting over a file reduces your iteration speed to less than a third.
RE/* syntactically valid, semantically wrong conversion */
/* const school = require('school'); */
let defaultId = 10;
let queryResult = (usePayload, payload) =>
if (usePayload) {
payload.student
} else {
/* no need for early return in Reason; if-else is an expression */
school.getStudentById(
defaultId
)
};
Types, Pass 1
Goal: correct the types, but just enough to move onto the next step.
You might still occasionally get syntax errors, but not as drastic as the previous step's.
Change
foo.bar
tofoo##bar
. This escape-hatch BuckleScript feature will be your medium-term friend.Convert
{foo: bar}
to[%bs.obj {foo: bar}]
(docs). Afterrefmt
, this will sugar to{"foo": bar}
.To communicate with external JS files, use
external
. They're BuckleScript's foreign function interface.Inline externals. No need to create clean, well-separated files for externals for now. We'll come back to these.
If it's too cumbersome to correctly type an
external
's input/output, use some placeholder polymorphic types, e.g.external getStudentById: 'whatever => 'whateverElse = ...
.For data types & patterns that are hard to properly convert over, you can occasionally create converters like
external unsafeCast : myPayloadType => anotherDataType = "%identity";
.
This is the first pass; the final types likely look different. For now, reap the rewards! Once you're finally done fixing all the type errors, your JS file should now be generated. Keep it open side-by-side. Time to come back and fix all the hacks!
RE/* syntactically valid, still semantically wrong, but better */
[@bs.module "school"] external getStudentById : 'whatever => 'whateverElse = "getStudentById";
let defaultId = 10;
let queryResult = (usePayload, payload) =>
if (usePayload) {
payload##student /* this will be inferred as `Js.t 'a` */
} else {
getStudentById(defaultId)
};
Runtime Semantics
Goal: fix the errors in the generated JS output.
Compare it with your old JS file. The output is likely incorrect; you probably mis-converted some idioms and mistyped some externals.
Type the shape of JS objects (the things that required
##
).Convert whichever parts to records/variants/idiomatic OCaml types.
All this time, check the output for any change.
REtype student; /* abstract type, described later */
[@bs.module "school"] external getStudentById : 'whatever => student = "getStudentById";
type payloadType = {. "student": student};
let defaultId = 10;
let queryResult = (usePayload, payload: payloadType) =>
if (usePayload) {
payload##student
} else {
getStudentById(defaultId)
};
Clean Up (Types, Pass 2)
Goal: make your types legit (aka, sound).
Go back fix whatever you've left during the first pass.
Make sure you don't have any
'whatever
types left inexternal
s.You can keep the
external
s inlined, or pull them out into a file.
RE/* in the current file */
type payloadType = {. "student": School.student}; /* TODO: put this somewhere else! */
let defaultId = 10;
let queryResult = (usePayload, payload: payloadType) =>
if (usePayload) {
payload##student
} else {
School.getStudentById(defaultId)
};
RE/* in a dedicated School.re file */
type student;
[@bs.module "School"] external getStudentById : int => student = "getStudentById";
[@bs.module "School"] external getAllStudents : unit => array(student) = "getAllStudents";
Type student
doesn't have an actual content; that's called an abstract type. It's a convenient way of specifying the relationship between external calls without knowing what the shape of the data is under the hood.
And then you're done!
Tips
Don't try to fully convert a JS file into a pristine Reason file in a single shot. Such method might actually slow you down! It's fine to have externals and bs.obj
left, and temporarily not take advantage of nice OCaml features (variants, labeled arguments, etc.). Once you've converted a few other related files, you can come back and now refactor faster by banking on the type system.
Whatever nice utilities you find, put them in a tempUtils.re
file or something. They're easy examples for your colleagues and removes some conversion churns.
We highly recommend you to check the JS output into version control. It makes your build system integration quasi-nonexistent, and makes sure that when you're not there, your teammates can make small changes, audit the output diff, and catch any mistakes. It's also a great selling point that the checked in JS output is friendly to emergency hot patches (a big selling point for managers!). Even if you're upgrading BuckleScript version, you'd catch any output difference. It's like Jest snapshots, for free!