Building Better ReasonML APIs with Chainable FunctionsUsing pipes and bindings for greater fluency and smoother error handling

chevron_leftAll Articles
Sam SlotskyEngineering @ Manifold

One of my favorite things as a programmer is consuming a clean API, and many of my favorite APIs can be described as fluent. This is a pattern where computations can be chained off of each other. For example, you may have heard of an object oriented design pattern called the builder pattern. First, let’s see what a non-fluent version looks like with some pseudo code from Wikipedia.

1Construct a CarBuilder called carBuilder
2carBuilder.setSeats(2)
3carBuilder.setSportsCar()
4carBuilder.setTripComputer()
5carBuilder.unsetGPS()
6car := carBuilder.getResult()

Above we are building a car, and we have one programming statement for each feature we want to add to it. But when I see this pattern out in the wild, I usually find that it’s implemented with a fluent API. Let’s see an example of a fluent builder API in C#:

1var builder = new FluentBuilder ();
2product = builder.Begin()
3 .Engine
4 .SteeringWheel
5 .Tire()
6 .Tire()
7 .Build();

Here we’ve built some kind of vehicle in just two statements: one to invoke the constructor, and the other to specify all the components that we want in our vehicle. It’s the return type of each of these methods that makes this possible. Each one has to return the builder so that you can keep calling the builder functions. If you write C#, you’re probably familiar with more types of chainable computations, such as IEnumerable<T> operations. Here’s one that multiplies a list of numbers by 3 and then returns the even ones.

Embedded content: https://gist.github.com/sslotsky/798a32fe5538200d9b6a98eadc51afab#file-postdata-js

Ruby programmers may also find this familiar, and it likely shows up in many languages. But when you start writing ReasonML, you may find yourself writing something like this to achieve the same result from a collection type:

Embedded content: https://gist.github.com/sslotsky/0caea40f98b28f365fd3bb888502a481#file-postdata-js

This is because Reason’s list type does not expose methods for performing computations on itself. Rather, there is a List module that exposes pure functions that operate on lists! But fear not: we can still achieve some fluency by using this wonderful thing called a pipe.

Using Pipes to Add Fluency

You might be familiar with piping from the command line on *nix operating systems, or if you’ve done much functional programming. If not, don’t worry, the concept is straightforward! A pipe merely takes the value on its left side and applies it as an argument to the function on the right! Let’s see an example for clarity.

Embedded content: https://gist.github.com/sslotsky/d6f5a3330ea400fcd05ab72e41d46ba9

Here we have a function that adds one to any integer, and two examples of calling that function using the number two as input. We can invoke our function in the standard way by calling add1(2), or we can pipe the number two into our function by calling 2 |> add1. So let’s apply what we learned to our list operations! First, here’s a trick you might want to know: List.map takes two arguments, but if I only supply the first one, the result will be a partially applied function. That means that List.map(someFunc) is actually a function that accepts a list and returns another list! That means I can do this:

Embedded content: https://gist.github.com/sslotsky/fe7026b12e8961e08d9efbfab0023892

If I take that a bit further, it also means that I can do this:

Embedded content: https://gist.github.com/sslotsky/6a174a7f29d2f6339eeba0199bf94083

Cool! We now have a single statement instead of three, and our function calls are less verbose. This feels a lot better to me, but in fact there is more we can do to make this API safe and enjoyable to consume. To see why it might be important to go further, before moving on to the next section, ask yourself this: what if one of these list operations threw an exception?

Fluent Error Handling

With the first rate type system and pattern matching that come with ReasonML, you can eliminate most types of runtime errors, but there are still some things that can blow up. For example, check out the description of List.tl in the ReasonML docs:

Return the given list without its first element. Raise Failure "tl" if the list is empty.

Let’s create a scenario where this can fail by multiplying our integers by two, filtering out the even integers (i.e. all of them), and then asking for the tail of the list.

Embedded content: https://gist.github.com/sslotsky/dc8958a48f8d44a4d3664cdb59295e6a

If you run this code, it will raise an exception because the list is empty when we call List.tl. ReasonML has a couple ways of dealing with exceptions. You can use a switch or a try/catch to handle this case. But we can actually make the error handling part of our fluent API by learning some new techniques!

Handle errors with a wrapper type

The first step towards improving our API is to wrap the type that we’re working with, so that we can represent both regular values and errors with a single type. ReasonML has a built in wrapper type called Option that would work fine for our purposes, but it’s really easy to make our own, so why not?

Embedded content: https://gist.github.com/sslotsky/005c2aa64e9cd04971aa12ca74076f8e

Here we are saying that there are two ways to represent a list: a Value that contains the list, or a Failure that we’ll use to indicate that a list operation raised an exception. We’d now like our list operations to return this type instead, but of course they don’t yet. So let’s start fixing that with the operation that’s giving us trouble: the tail call.

Embedded content: https://gist.github.com/sslotsky/2a3c5698a365554567e9c66ea10e1b40

So now if we call tail instead of List.tl, we’ll get our Failure type instead of an actual exception. But this doesn’t allow us to chain our calls! I can’t pipe the result of tail into another function that operates on a list, so I don’t have a fluent API. Fortunately, we can get this capability back by learning about the bind operator.

Function Chaining Unbound!

ReasonML has an operator that looks like >>= and goes by the name bind, and we use it for performing chainable computations on wrapper types. We get to define how this operator works according to the following rules:

  1. It accepts the wrapper type as the first argument.
  2. The second argument is a function that takes the type we want to wrap, and returns the wrapper type.
  3. It must return the wrapper type.

That means that our bind operator will need to accept a chainableList as a first argument, and a function list => chainableList as a second. Here’s how we’ll define ours:

Embedded content: https://gist.github.com/sslotsky/385ba5c22cbc48407fb395d30ac14693

Let’s break this down! The m argument is our incoming chainableList, and our f argument is the function that we want to apply to the list that we’ve wrapped. As long as m contains a list, it’s safe to call the function, but if m is a Failure then we can just return a Failure. So what does this give us for our API? Let’s take a look:

1[1] |> tail >>= tail >>= tail

Here we start by piping a list with a single element into our tail function. This is going to return a Value that contains an empty list! After that, we use our bind operator to keep feeding the result of tail into subsequent tail calls. The second and third call will return Failure and we won’t have to worry about recovering from exceptions. We can chain calls to tail as many times as we want and our app will never explode!

So how about all those other operations, like map and filter? None of them throw exceptions, so what we can do is define a generic function for wrapping operations that won’t fail. I’m actually going to use two functions to do this:

Embedded content: https://gist.github.com/sslotsky/bfd44a4f0af31846dc9f8b6628e7392f

A return function is often used in scenarios like this as a way to wrap simple values in a wrapper type, but for our case we will get more mileage if we build off of it using the wrap function defined above. The first argument to wrap is a function that takes a list and returns another list, which is exactly what our computations like map and filter do. The second argument is the list that we feed into the function f. We then use the return function to wrap the output of this computation in a Value to convert it to our chainableList type. Now our fluent API looks like this:

Embedded content: https://gist.github.com/sslotsky/6f12f40bef469c7a4f8fac96c5b0ec96

Let’s break it down!

  1. Wrapping our list in a return call converts it to a chainableList that we then feed into the subsequent operations using the bind operator.
  2. The first wrap call is our map operation. List.map(n => n * 2) is a partially applied function; it accepts a list and returns another list, and is therefore the type we need for the first argument of wrap. The same is true of our filter operation on the following line.
  3. But our calls to wrap are also missing their second arguments! The wrap function is supposed to take a list! Therefore, it’s a function that takes a list and returns a chainableList, which is the type expected for the second argument of our bind operator!

So on one hand, it might seem like a lot to unpack because we have so many functions returning other functions. But take another look at the result! We’ve achieved a pretty darn fluent API that’s also safe to use without fear of runtime errors! And guess what? There’s a bonus!!

Augmented Operations

So far I’ve been selling this bind operator as a way to create fluent APIs that are safe to use. But now that all of our operations are going through this bind function, we can add some behavior to it. For example, we might want to do some logging. If our operations are asynchronous or computationally expensive, we might want to gather metrics on how long they take and how often they fail. We can do this in one convenient place without sacrificing the friendliness of our API!

Embedded content: https://gist.github.com/sslotsky/f02dcdd5ea00faa41eb1c6c0202cf679

Conclusion

Today we learned about building fluent APIs in ReasonML. We started with a technique called piping to add fluency to a series of functional operations. We then took that concept further by using binding and wrapper types, which allows us to handle exceptional cases neatly and also augment operations going through our pipeline with additional functionality. Check out my sketch to see all the code we wrote today, and make sure to fork it and play around with it too!

Notes

In this post I’ve used a technique that is well known among functional programming enthusiasts, and I have intentionally avoided mentioning it by name. I won’t go so far as to say it’s forbidden here, but I often find discussion of this technique to be highly theoretical and lacking in pragmatism, so I’m trying to reverse the approach and talk about the problem we’re solving first. If you already know the technique, feel free to congratulate yourself quietly, but please try not to scare others away!

Further Reading

Some related things you might want to read about:

  1. If you are new to ReasonML and looking to learn more, the docs are good and getting better all the time.
  2. Check out this example of a fluent builder API in C# to learn more about the builder pattern, fluent APIs, and object oriented design.
  3. This post was inspired by a series about computation expressions from F# for fun and profit. If you’re unfamiliar with F#, it’s Microsoft’s version of OCaml, so the material translates quite well since ReasonML is just an alternative syntax for OCaml. I really recommend trying it out, and this website is a great place to learn about it. The material there is top notch, and this series is no exception.
  4. The technique I describe in this post is also a form of what’s known as railway oriented programming, which you might think of as a way to short circuit your programming flow without relying on early returns.
  5. All of the code we wrote for this example can be found in this sketch. Please fork it and play around with it! Sketch is a fantastic and simple online REPL for ReasonML, and I’ve absolutely loved using it.
  6. If you are interested in the future of async/await in ReasonML, or if you enjoyed the F# post on computation expressions that I linked above, you’ll definitely want to keep an eye on let-anything by Jared Forsyth!
Stratus Background
StratusUpdate

Sign up for the Stratus Update newsletter

With our monthly newsletter, we’ll keep you up to date with a curated selection of the latest cloud services, projects and best practices.
Click here to read the latest issue.