Writing JavaScript libs in OCaml?

interop

#1

Hi, I need to write pretty simple lib with cli wrapper for JavaScript/TypeScript projects. It needs to read few files from disk, do some logic and print the output to console. It needs to be npm installable.
I would like to write it in OCaml/Reason but there is the question: what’s the lowest possible dependency and runtime cost for using it from plain JS project? What is the difference of using JSOO/BuckleScript for this?
Do you know any libs written like this?


#2

Hi there. Let me start with a quote from a BuckleScript blog post ( https://bucklescript.github.io/blog/2018/12/05/release-4-0-8 ):

If we ignore the C shims, the BuckleScript runtime is very small, and it is pretty easy for experienced BuckleScript programmers to write runtime-free code which generates standalone JS code. Such code could include supporting curried calling conventions, encoding of OCaml ADT, etc.

In my experience this is true. For example, I wrote a utility script that does a little post-processing of some generated HTML documentation: https://github.com/yawaramin/bs-webapi/blob/80c186bc64b36de5dd7d792e09731a2d8e7ad8f3/src/Yawaramin_BsWebapi/Yawaramin_BsWebapi_OCamldoc.ml

It lists the files in the current directory, and for each HTML file it reads it, processes it, and writes the result back to the same file.

This script targets JavaScript and Node and ignores the BuckleScript runtime. It generates a JS output with no dependencies on anything distributed from bs-platform. I made a conscious choice to do it like that. Here are two examples:

  • A simple example: instead of using the OCaml standard library’s Array.iter (which generates a bs-platform dependency), I used Js.Array.forEach, which directly calls forEach on the array.
  • A slightly more complex example: instead of using Printf.sprintf, I wrote a binding to Node’s util.format() and used that directly. The reason I couldn’t use BuckleScript’s built-in interpolation in that case was that my input string was a regex replacement string that contained numbered captures like $1 and BuckleScript would have tried to find a name $1 in scope and failed.

So yes, you can do it with a little care. Keep checking your output for require(...anything from bs-platform...) and find a way to work around those.


#3

I was wondering is it worth to try and it looks possible now. Thx for the hints.


#4

What are the main tricks to writing code that doesn’t rely on bs-platform? I feel like the bucklescript runtime is used all over, at least for currying, no?

Also, that means no using Belt, right?


#5

My main strategy is looking at the output and working around whatever imports I see there. It’s relatively easy to not depend on Array because there’s a JS equivalent; but for some other things it’s definitely tougher, e.g. handling options and undefined in the general case. For those I don’t have a general strategy. Mainly just trying to figure out if it can be rewritten in another way to not have those dependencies.

That said, currying is IME not used that often (despite being tremendously useful when it is). With Reason’s fantastic syntax sugar for uncurried functions, I just use those a lot when I need to pass first-class functions over to JS libs.

At the end of the day though, the most important question is whether I actually need to eliminate bs-platform runtime dependencies. Because I can use Docker images with bs-platform pre-installed for building, and I can create bundles for deploying, it’s almost never the case that I do. For now it’s mostly just an academic exercise.


#6

Interesting thread. I am wondering: Is it worth the trouble to eliminate the dependencies? I mean if bs-platform has a small runtime, then it can easily just go into the node_modules when your pkg gets installed by npm, right?

PS Yawar- thanks for your Learn Type-Driven Development book, just finishing working though the example codes now.


#7

The real annoyance is that the js runtime is not a separate package from the compiler, so the programmer either needs to bundle all of their server side code, which can be complex or difficult, or ship using docker, or take the time to build the ocaml compiler on deploy, or clean the node modules folder before deploy. The bs-platform folder weighs in at about 200 MB iirc.


#8

@yawaramin you’re right, I don’t need to, because of Docker or bundling. But it would be very nice to be able to tell Node devs that they can deploy using their same old Node platforms (I think Heroku would have a hard time with Reason?) without the pain of server side bundling. Especially for things like lambda functions.


#9

For sure. I think there’s an open issue to split off the platform libraries into a separate npm package. But in the meantime I wonder how difficult it might be to ‘roll our own’, i.e. upload the output JS libs to a separate project and rewrite the requires/imports to use those.

For example, a function like this:

let f(x) = x
  |> Js.Nullable.toOption
  |> Js.Option.getWithDefault(1);

Will generate the requires:

var Js_option = require("./stdlib/js_option.js");
var Js_primitive = require("./stdlib/js_primitive.js");

Now suppose there was a package @bs-platform/stdlib with compiled JS files for the version of BuckleScript we’re targeting. We’d just need to rewrite these requires to:

var Js_option = require("@bs-platform/stdlib/js_option.js");
var Js_primitive = require("@bs-platform/stdlib/js_primitive.js");

It’s hacky but it could work!

Edit: looks like Moox already did something like this: https://www.npmjs.com/package/@phenomic/bs-platform


#10

Thanks, btw–I hope you enjoy the book!


#11

I think @jaredly did something like that also :+1:, and his strategy is to rewrite the package.json at deploy time!


#12

Interesting. I wonder if I’m missing something and it’s not really as difficult as rewriting the outputs. If there’s an easier way it would definitely make deploying on the backend more palatable.