Why ReasonML? (Part 1)

John Haley
7 min readOct 11, 2018

--

After being in the functional programming world for long enough, it’s easy to lose sight on why somebody would even be interested in FP in the first place. Sure enough I’m not an exception to this and so much of this world seems self-evident to me. This is a problem for many people starting out since the experienced devs/people they go to for questions usually just stare back at them and wonder if it’s ok to answer with links to LMGTFY.

There was an intro to ReasonML workshop recently that jumped straight into the “how” of ReasonML. Finally, when somebody piped up and asked “why?” nobody was really able to answer the question. This is a bad thing. Even when things seem obvious you should take the time to learn why you’re doing it. This post is all about addressing that. And addressing that specifically to the modern JavaScript dev.

JavaScript is incomplete

TypeScript/Flow (static typing), Lodash/Ramda (generic utility library), Babel (build system), React (UI framework), and Redux (state management) are all things we use to supplement things we need that core ECMAScript doesn’t have. How ubiquitous these things are (and in fact how necessary they feel) shows that there are features that are missing in ES that most people need or at least want.

And all of the things above are things that ReasonML eloquently solves.

Static Typing

Where does TypeScript/Flow fall short?

  • Verbose syntax
  • Type inference in TypeScript/Flow is ok, however, being built on top of JavaScript severely limits it
  • TypeScript is unsound by design. E.g. TypeScript permits an incorrect program to pass type checking
  • Neither enforce handling of all cases
  • Compile time can be slow

Flow, while being still in use, has fallen behind TS in recent years and community support is much smaller so we’ll focus on TS in this post.

One of the things I remember really liking about getting into JavaScript was the terseness of the syntax. Going from C# which looks like:

public static int add(int a, int b)
{
return a + b;
}

To code that looked like this:

const add = (a, b) => a + b;

Really helped make the code look less busy. Now the above can be written in TS two different ways:

const add = (a: number, b: number) => a + b;// Orconst add = (a, b) => a + b;

The first way has all of the types listed on the arguments and it’s able to infer the return result is also a number. The second way loses all types of anything that is passed in. So even if you pass in 2 numbers you’re going to get an any type out which can cause all sorts of havoc and can make functions that are called later break in ways that you wouldn’t expect a type system to break (Note that there is a flag you can enable to prevent this).

TypeScript expects type definitions to be declared by default and only in special cases can it infer what the types should be.

Now let’s see how to write that in ReasonML

let add = (a, b) => a + b;

Looks very similar to the original JS version!

ReasonML is able to infer that we are taking in 2 arguments of type int and returning a type int. Type annotations are not needed by default. You can include them if you choose but you only have to in special cases. The syntax is much more terse.

Now this is a simple example, but type signatures can get very busy for real applications. With ReasonML, a lot of that is unnecessary and your code can remain simpler to read.

Type Inference changes everything

An added benefit of type inference that is rarely gone over is that you can get the benefits of both dynamic typing and static typing. How? With not having to explicitly write out all of your types from the start you can get right into coding. Prototyping is a breeze. If you need to go back and change something it doesn’t matter that you didn’t annotate anything because ReasonML will still be able to figure out if the change you made will break something anyways.

A Sound Type System

What does this even mean? Why should I even care?

To a seasoned FP developer these questions are frustrating. Frustrating not just because they sound naive, but frustrating because they are totally valid questions that are hard to answer. However, we’re developers, which usually means we do dumb things and are stubborn as hell so let’s ignore the advice of those that came before us and try to answer these questions anyways.

A sound type system guarantees the program will be correct if the types are true

Aww come on! More crazy theoretical jibba jabba. Fair enough, let’s break this down.

A sound type system guarantees the program will be correct

This is saying that during runtime, your program will get the types it expects and all of your type definitions will be accurate. Since you can rely on the types you can make decisions/optimizations/etc… on a solid foundation.

… if the types are true

If we haven’t lied about the types, then our guarantee is good. If we have then our program still may be correct, but it’s not guaranteed. How could we lie about our types? One example is any time we call out to a library we are writing some bindings to that library where we declare types and pinky swear that they are true. If they aren’t then we don’t have our guarantee.

TypeScript is Unsound

Even if you never lie about your types, with TypeScript you will never get the above-mentioned guarantee. TypeScript developers have made perfectly valid design decisions to make the type system unsound for pragmatic reasons. Luckily for us, ReasonML is both pragmatic and sound at the same time.

Ok so still, what’s the big deal?

The big selling point with a type system is the confidence it gives you in your code. The confidence you gained by going from JavaScript to TypeScript is similar to the confidence you can gain by going from TypeScript to ReasonML.

When I make a change to a TS/JS codebase and it works on the first time? I feel uneasy. When I make a change to a ReasonML codebase and it works on the first time? It’s just another day at the office.

Just don’t do this

Notify about missing or unused cases

With having a rock-solid type system, we can do some really great things. An example of this is the ability to notify the developer that they could get a value that is not handled in a function. Or even notify a developer that a case they are handling won’t ever be called!

Let’s take this TS snippet:

type Id = string | number;function getIdString(id: Id) {
let result = "internal-id-";
if (typeof id === "string") {
result += id;
} else if (typeof id === "number") {
result += id + "-number";
}
return result;
}

Now suppose that we receive new requirements where the id might be some array of strings that we need to concatenate. If we change the Id type to be type Id = string | number | string[]; everything still works just as before and the developer who made the change has to resort to searching the codebase to find all of the code they have to update which leads to many of the Cannot <something> of undefined errors we see so often.

In ReasonML the developer would get a warning saying that a block of code no longer handles all possible cases. For extra strictness you can set a compiler flag to treat these as errors if you want.

Now say we go the other way, What happens if change the type to type Id = string? In the above TypeScript code we’ll get no notification that anything could be optimized. We have code that will never get run and we’ll never know about it unless we manually audit this part. In ReasonML, the compiler will tell us if a part of the code is unreachable and we can choose how to handle it at that point.

Fastest Build System in the West

While some users of TypeScript have noticed a slow down with larger projects, at Hashback we have a repo with about ~40k lines of ReasonML code. A full clean and rebuild of the project takes < 5 seconds. In the time it takes me to switch to a different terminal tab and run my test watcher the build system was able to clear out all of the previous build artifacts and remake every single file.

And when I’m rebuilding on changes? It’s done before I can move my eyes down to look at the integrated terminal.

How is the build system so fast? It’s based off of Ninja which was written because the build for Chrome was taking too long.

Benefits of ReasonML over TypeScript

To summarize, the benefits to be gained by using ReasonML are:

  • Syntax that is simple but still gives the benefits of strong static typing
  • Syntax that is very familiar to JS and TS devs
  • A sound type system with very little effort (Type Inference!)
  • Prototyping is as fast as with a dynamic-typed language
  • A build system that lets you know if you haven’t handled a potential case or if you can remove code
  • A build you will never have to wait on

Next Up

In the next part, we’ll focus on utility libraries like Lodash/Ramda and why they’re used. And still, why ReasonML?

Edit: Here’s the next part:

--

--

John Haley

Husband, Father, Developer, Musician, Nerd and Powerlifter. Former Product Owner and Co-creator of @GitKraken. Current co-founder of @hashbackio