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.pipebinding 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 insrc/FFI/Express.ml.(NOTE: Remember to includesrc/FFIin thesourcesfield ofbsconfig.json!)

type app
external express : unit -> app = "" [@@bs.module]
external listen : app -> int -> unit = "" [@@bs.send]
type req
type res
external get : app -> string -> (req -> res -> res) -> unit = "" [@@bs.send]
external send : res -> string -> res = "" [@@bs.send]

Runningbsbresults in the followinglib/js/src/index.js:

// Generated by BUCKLESCRIPT VERSION 1.8.2, PLEASE EDIT WITH CARE
'use strict';
var Express = require("express");
var app = Express();
app.get("/", (function (_, res) {
return res.send("Hello, world! <a href='/page'>Page 2</a>");
}));
app.get("/page", (function (_, res) {
return res.send("Hey <a href='/'>Go back</a>");
}));
app.listen(1337);
exports.app = app;
/* app Not a pure module */

Nice! We can runnode lib/js/src/index.jsand get ourselves a running express server.

The Chaining Express API

Consider the type we wrote for theExpress.getfunction:

external get : app -> string -> (req -> res -> res) -> unit = "" [@@bs.send]

gettakes anapprepresenting our express instance, astringfor the path, a function (which takes a request and response), and returns a no-op (typeunit).

However — did you know we can_chain_this API like so? In JavaScript:

app
.get("/", (req, res) => res.send("Hello, world!"))
.get("/about", (req, res) => res.send("About ..."))
.listen(1337)

This pattern is very common in JS, and works in the following way: instead ofgetaccepting anappand returning aunit(or no-op), we return anotherappwhich we can then use on a subsequentget!

That’s a lot to unpack, so let’s demonstrate how to get from A to B in code.

Step 1: Take an app, return an app

external get : app -> string -> (req -> res -> res) -> app = "" [@@bs.send]
let f: app = get (express ()) "/" index;;
let g: app = get f "/about" about;;
listen g 1337;;

So what’s different here? First, we changed the return type ofgetfrom aunitto anapp. Next we remove the definition forappand inlineexpress ()infdirectly.

Then, instead of usingappas the first argument for our second call toget, we pass inf. This is type-safe (remember:f,g, andexpress ()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 forfentirely like so:

let g: app = get (get (express ()) "/" index) "/about" about;;
listen g 1337;;

Or a step further, inlininggas well:

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

These two examples are_identical_to the first, but notice thatappis only referenced once in our code. Let’s peek at BuckleScript’s outputlib/js/src/index.js:

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

🔗🔗🔗🔗🔗🔗🔗🔗!!!

See, once we smush together ourgetandlistencalls, there’s no need for temporary variables likefandg. 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 specifiesappmultiple 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 inliningfandgin 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…”)

To remedy this, OCaml provides the infix|>(or “pipe”) operator. We can inspect its type viautop :

utop # (|>);;
- : 'a -> ('a -> 'b) -> 'b = <fun>

We see that we take an item of typea, a function fromatoband return an item of typeb._*Exhale*_In code:

f(x) === x |> f

And if we were to use this pipe multiple times:

f(g(x)) === x |> g |> f

We can see here how the pipe operator (|>) allows us to unfold various layers of function composition. It’s quite neat, and leads to some very readable code. Let’s use it with our example above:

apply_discount(
(get_age_group(get_age(user_from_id(id))))
price)
(* turns into... *)
apply_discount(
(id |> user_from_id |> get_age |> get_age_group)
price)

How about that last layer? What if we wanted to unfoldapply_discountas well?

let f = id |> user_from_id |> get_age |> get_age_group |> apply_discount;;
f price;;

Decent! However we hit a snag.apply_discounttakes_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 becausepricewould be used as the_first_argument toapply_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 makepricethe first argument!

Step 3: Save the app for last

If we were to redefineapply_discountfromgroup -> price -> totaltoprice -> 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 toapply_discountfrom 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 withapply_discount:

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

Notice how|>doesn’t really buy us much. Since anapptype must be the first argument togetandlisten, we’re left with a confusing mix of parentheses and|>operators.

As we learned in the previous section, our solution is tomove this argument to the end. Let’s try it with some helper functions:

let get_ route handler app = get app route handler
let listen_ port app = listen app port

And use ’em like so:

express () |>
get_ "/" index |>
get_ "/about" about |>
listen_ 1337

And voila! Anapptype makes it way fromexpress (), through the pipe and onto the end ofget_ “/" index. That method also returns anapptype, which finds its way at the end ofget_ “/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 afunction_for everyfunctionyou bind to JavaScript-land doesn’t sound all that exciting, though. Wouldn’t it be great ifgetandlistencould work like that for us? Well they can!

The current bindings forgetandlistenare defined using the@@bs.sendattribute as follows:

external listen : app -> int -> unit = "" [@@bs.send]
external get : app -> string -> (req -> res -> res) -> app = "" [@@bs.send]

However, BuckleScript also provides us with a@@bs.send.pipewhich, you guessed it, allows us to define functions that work well with the|>operator.From the docs:

bs.send.pipe
is similar to
bs.send
except that the first argument, i.e, the object, is put in the position of last argument to help user write in a
chaining style
:

Here’s a modified binding forget:

external get : string -> (req -> res -> res) -> app = "" [@@bs.send.pipe: app]

The difference here is that the firstappin the type definition has been moved into the attribute, right after@@bs.send.pipe: . Here’s our new definition forlisten:

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

Now, we can swap outget_andlisten_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.pipeworks, 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 forgetisreq -> res -> res Why the secondres Well, express has
    operations onreslikesend, status, andcookiewhich are also chainable (they return arestype).
    Write chainable bindings for these methods.
  • Imagine@@bs.send.pipedid not exist and we were stuck with our old definitions ofgetandlisten: could we create a function calledmake_chainablewheremake_chainable get === get_andmake_chainable listen === listen_?
    Why or why not? (As a hint: what ifgetandlistenboth had three arguments, could we do it then?)

Source: https://medium.com/dailyjs/typesafe-javascript-chaining-with-ocaml-and-bucklescript-ff489fe287c2