Typesafe JavaScript Chaining with OCaml and BuckleScript

In my previous article, we explored how BuckleScript
allows you to turn OCaml code into readable JavaScript, and how to interface
with other modules in the JavaScript ecosystem.

Today I’d like to continue on this path and show you the awesome
@@bs.send.pipe binding attribute, which enables us to write concise OCaml code
to interface with JavaScript libraries that have a chainable API.

Exhibit A: Express

To interface with the express Node.js web framework,
we may write the following bindings in src/FFI/Express.ml. (NOTE: Remember to
include src/FFI in the sources field of bsconfig.json!)

Step 1: Take an app, return an app

So what’s different here? First, we changed the return type of get from a
unit to an app. Next we remove the definition for app and inline express
() in f directly.

Then, instead of using app as the first argument for our second call to get,
we pass in f. This is type-safe (remember: f, g, and express () all have
the same type) and sure enough if we compile this script and run it — we get a
working Express app!

In fact, if we wanted to, we could start combining some of these lines by
inlining the definition for f entirely like so:

These two examples are identical to the first, but notice that app is only
referenced once in our code. Let’s peek at BuckleScript’s output
lib/js/src/index.js:

Express().get("/", index).get("/about", about).listen(1337);

🔗🔗🔗🔗🔗🔗🔗🔗!!!

See, once we smush together our get and listen calls, there’s no need for
temporary variables like f and g. BuckleScript knows this, and merely puts
everything inline for us — in a “chained” manner.

This may start to look a little LISP-y to you, and that’s fair — this syntax is
not easier to read than our original example which specifies app multiple
times. Let’s move on and see how we can clean up this code a little.

Step 2: Some light plumbing, and a leak

As we start composing functions (like we did by inlining f and g in the
previous section), we’ll start to see quite a bit of parentheses. Consider the
following bit of code:

apply_discount(
(get_age_group(get_age(user_from_id(id))))
price)

Sure we can dress this up with further indentation, but developers reading this
code will still construct a sort of “stack” in their head as they read the
subsequent functions from left to right (“Okay apply discount of the age group
of the age of the…”)

Decent! However we hit a snag. apply_discount takes two arguments, the
user’s age group, and a price (group -> price -> total). If we were to write
our code like so:

... |> get_age_group |> apply_discount price

We would receive a type error because price would be used as the first
argument to apply_discount. This means we need some parentheses (technically
you could use OCaml’s @@, but hold your horses), which we are trying to avoid!

(... |> get_age_group |> apply_discount) price

One way to fix this? Just make price the first argument!

Step 3: Save the app for last

If we were to redefine apply_discount from group -> price -> total to price
-> group -> total, we could then remove our parentheses entirely:

... |> get_age_group |> apply_discount price

Now price is used as the first argument, and second argument (the age group)
makes its way to apply_discount from the pipeline.

“Jordan this is great but I don’t really care about discounts and age groups,
I’m trying to write a web server before my startup goes under.”

Well fear no more, let’s return to our express example from earlier.

listen
(get
(get (express ()) "/" index)
"/about"
about)
1337

If we were to swap in some |> operators, we’ll quickly run into the same exact
problem we had with apply_discount:

And voila! An app type makes it way from express (), through the pipe and
onto the end of get_ “/" index. That method also returns an app type, which
finds its way at the end of get_ “/about" about, and so on and so forth. We
now have ourselves a beautiful, type-safe chain of functions that map to the
chainable express API.

Express().get("/", index).get("/about", about).listen(1337);

Step 4: BuckleScript can do this for us

Defining a function_ for every function you bind to JavaScript-land doesn’t
sound all that exciting, though. Wouldn’t it be great if get and listen
could work like that for us? Well they can!

The current bindings for get and listen are defined using the @@bs.send
attribute as follows:

The difference here is that the first app in the type definition has been
moved into the attribute, right after @@bs.send.pipe: . Here’s our new
definition for listen:

external listen :int -> unit = "" [@@bs.send.pipe: app]

Now, we can swap out get_ and listen_ in favor of their original
counterparts.

express () |>
get"/" index |>
get"/about" about |>
listen 1337

🎉🎉🎉🎉🎉🎉

Closing Thoughts

Okay so that was a lot of words to tell you how @@bs.send.pipe works, but I
hope this post gave you a bit of intuition for why it exists and why you may
want to use it. With that, here a few more questions to ponder on:

You may have noticed that the type of the callback for get is req -> res ->
res. Why the second res? Well, express has
operations on res like
send, status, and cookie which are also chainable (they return a res
type). Write chainable bindings for these methods.

Imagine @@bs.send.pipe did not exist and we were stuck with our old
definitions of get and listen: could we create a function called
make_chainable where make_chainable get === get_ and make_chainable
listen === listen_? Why or why not?(As a hint: what if get and
listen both had three arguments, could we do it then?)