In a real-life, decently-sized codebase, one cannot simply convert from one technology to another within a few days, weeks, or sometimes months or years. If the interop isn't carefully designed enough to wiggle the language into a codebase, the conversion cost could dwarf the initial value of the language and become a deal breaker.
A well-crafted interop system that is simple, performant, local and non-prescriptive allows devs to use familiar concepts while trying out the technology, to be confident enough of the (lack of) unknowns to recommend the solution to managers, and to be productive while converting the team's project.
The above section might sound a bit buzzword-y; here's is how we've designed BuckleScript's interop, from the lowest part of the stack to the highest.
Most variables' name compile to clean JS names.
hello compiles to
hello, whereas other compilers might turn it to e.g.
__$INTERNAL$hello$1. Clean names (i.e. "no name mangling") help debugging and one-off experiments & bindings. We've seen emergency fixes where a team mate less familiar with BuckleScript and Reason comes into a file and drop a raw JS code block in the middle of a BuckleScript file. It's reassuring to know that last-resort escape hatches are possible.
Major data structures in BuckleScript map over cleanly to JS. For example, a BS string is a JS string. A BS array is a JS array. The following BS code:
let messages = [| "hello"; "world"; "how"; "are"; "you" |]
let messages = [|"hello", "world", "how", "are", "you"|];
Roughly compiles to the JS code:
var messages = ["hello", "world", "how", "are", "you"]
There is zero mental and performance overhead while using such value. Naturally, the value on the BuckleScript side is automatically typed to be an array of strings.
This behavior doesn't hold for complex data structures; the dedicated sections for each offer more info.
In most cases, you can directly call a JS function from BS, and vice-versa! These calls are free of performance overhead in most cases.
let declarations in a BS file is exported by default and usable from JS. For the other way around, you can declare in a BS file what JS module you want to use inside BS. We can both output and consume CommonJS, ES6 and AMD modules.
BS comes with a lightning-fast (fastest?) build system. It starts up in a few milliseconds and shuts down as fast. Incremental compilation is two digits of milliseconds. This allows the build system to be inserted invisibly into your whole JS build pipeline without embarrassing it. Unless your JS build pipeline is already embarrassingly slow. That's ok.
1 BS file compiles to 1 JS file. The build can be configured to generate JS files alongside or outside your
re source files. This means you don't have to ask the infra team's help in trying out BuckleScript at the company; simply generate the JS files and check them in. From the perspective of the rest of the compilation pipeline, it's as if you've written these JS files by hand. This is how Messenger successfully introduced Reason into the codebase.
We use NPM and Yarn. Since the generated output is clean enough, you can publish them at NPM
prepublishOnly time and remove all trace of BuckleScript beforehand. The consumer wouldn't even know you wrote in BS, not JS! Check your
node_modules right now; you might have been using some transitive BS code without knowing!
Hopefully you can see from the previous overview that we've poured lots of thoughts into simplicity, familiarity and performance in the interop architecture. The next few sections describe each of these points in detail.