aboutsummaryrefslogtreecommitdiff
path: root/hidden/2017-08-19-typesafe-javascript-chaining-with-ocaml-and-bucklescript.md
blob: 9d9ab22bd5eb3b72d61a109f8fd75b7b0ba678f3 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
---
title: Typesafe JavaScript Chaining with OCaml and BuckleScript
route: /js-ocaml-chaining
date: 2017-08-19
description: Let's write some concise OCaml code to interface with JavaScript libraries that have a chainable API.
---

![OCaml code: express () |> get "/" index |> get "/about" about |> listen 1337](https://cdn-images-1.medium.com/max/1600/1*JE53vATHfCSAXLXrXSvgKA.png)

In my [previous article](/js-ocaml-microservices), 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](https://expressjs.com/) 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`_!)_

    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]

Then, in `src/index.ml` we could use this code as follows:

    open Express

    let app = express ();;

    get app "/" (fun _ -> fun res ->
        send res "Hello, world! <a href='/page'>Page 2</a>");;

    get app "/page" (fun _ -> fun res ->
        send res "Hey <a href='/'>Go back</a>");;

    listen app 1337;;

Running `bsb` results in the following `lib/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 run `node lib/js/src/index.js` and get ourselves a running express
server.

### The Chaining Express API

Consider the type we wrote for the `Express.get` function:

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

`get` takes an `app` representing our express instance, a `string` for the path,
a function (which takes a request and response), and returns a no-op (type
`unit`).

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 of
`get` accepting an `app` and returning a `unit` (or no-op), we return another
`app` which we can then use on a subsequent `get`!

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 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:

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

Or a step further, inlining `g` as well:

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

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…”_)

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

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

We see that we take an item of type `a`, a function from `a` to `b` and return
an item of type `b`. \*_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 unfold `apply_discount` as well?

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

    f price;;

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`:

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

Notice how `|>` doesn’t really buy us much. Since an `app` type must be the
first argument to `get` and `listen`, we’re left with a confusing mix of
parentheses and `|>` operators.

As we learned in the previous section, our solution is to **move 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! 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:

    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.pipe` which, you
guessed it, allows us to define functions that work well with the `|>` operator.
[From the docs](https://bucklescript.github.io/):

> `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 for `get`:

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

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](https://expressjs.com/en/4x/api.html#res.append) 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?)_