DEV Community

Brian Neville-O'Neill
Brian Neville-O'Neill

Posted on • Originally published at blog.logrocket.com on

What makes ReasonML so great?

What exactly is Reason?

ReasonML is a syntax extension for the OCaml language created by Facebook_._ Rather than creating an entirely new language, the creators of Reason chose to build on top of OCaml, a battle-tested functional systems programming language that’s been around since the late 1990s.

In fact, Reason has the same roots as React (which needs zero introduction in the world of 2018 web development). Many of the initial React prototypes were done with language very similar to OCaml (Standard ML), and Reason and ReactJS share the same creator!

In addition, Facebook has used Reason on Messenger.com for quite some time now, so as we’ll see, the evolution of the language is one of practical needs rather than lofty ideals.

Similar to what they’re doing with React, Facebook tests all of the new additions to the language internally before they actually add the language

Personally, I love this about Reason—the language was created to solve real-world problems in production-grade applications. And, as you might have guessed, production isn’t always the best place for “experimental” anything.

What makes Reason so great?

It supports native and JavaScript as compile-targets

This is the holy grail that a bunch of modern languages/frameworks are looking for. Supporting native and JavaScript as compiler targets allows code to be “written once, run anywhere”.

Since OCaml already compiles down to assembly, native support is built-in. Reason supports compilation into decently readable JavaScript via the BuckleScript project, which was created over at Bloomberg as a way to write their front-ends in OCaml. Because Reason is, essentially, OCaml, adding support for JavaScript compilation came for “free” with the OCaml ecosystem.

Since Reason supports JavaScript as a compile target, it has a way to talk to existing JavaScript code via a FFI (foreign function interface). This ensures that the types stay true while allowing you to speed up development by using the libraries that you already know and love.

In fact, because of this FFI interoperability with JavaScript, Reason already has React bindings!

A rock-solid type system

As primarily a front-end JavaScript developer coming to Reason, this wasn’t something I was used to at all. Since JavaScript is a dynamically-typed language with type-coercion, you can inevitably end up one of 2 scenarios creeping into your codebase.

Runtime errors

One scenario that you can land in with dynamic typing is runtime errors due to type mismatches. Ever seen the undefined is not a function error when you tried to click something? Or Cannot read property 'x' of undefined? Both of these errors come from trying to operate on sections in your code in ways that they weren’t intended to be used. For example, calling Array.prototype.map on null will throw an error, and in some cases can even crash your application.

Granted, we definitely don’t want type errors to crash our application. However, avoiding these errors is really difficult, especially if you have a large application with lots of dynamic data coming from backend APIs.

Checking and testing

This leads us to the second scenario that you’re likely to find yourself in in a dynamic application: lots of type-checking and testing to make sure the data flowing through your application is exactly what you expect. If it is, you’ll often see code that looks something like this:

// `myData` is expected to be an array of strings, but sometimes it can return as `null` from the backend

if (Array.isArray(myData)) {
  // operate on the data
}

However, it doesn’t stop at dynamic data coming from APIs. Many times when refactoring a module the way it interacts with the rest of the application might change. If you don’t do your due diligence and update everything that depends on what you refactored, you’re also running the risk of runtime errors.

In these scenarios, you’d better hope you have a rock-solid test suite to help you figure out what broke. Doing these types of refactors in JavaScript can be treacherous, especially in a larger application.

However, in a soundly-typed language like Reason, many of these runtime problems are converted into compile-time problems. Instead of having to worry about your app crashing because you forgot to add that one extra function parameter, you’ll get a compiler error. This means you can cut out all of the runtime type-checking and just write your code to do what you want it to do.

Typescript, Flow, and verbosity

Right now you might be thinking, “What about TypeScript and Flow?” — after all, they don’t carry the overhead of an entire new syntax along with them. However, while it is possible to achieve a lot of security in typed JavaScript, that doesn’t mean it’s easy. The type system tends to be only as strong as you, the developer, make it, and when you’re in crunch mode and the compiler is yelling at you you’re much more inclined to start typing all your code as any type just to ship your code. In addition, typing everything to achieve that extra security can get rather verbose in TypeScript, in my opinion.

Reason’s type system is rock-solid, and because the compiler infers most of the types of what you write it tends to not be very verbose. As I’ve been playing around with Reason I’ve found it a very pleasant experience to have the compiler catch most of my errors, shortening the feedback loop andshowing me what I did wrong. Rather than getting a blank screen after clicking a button with an incorrect handler, I get a compiler error telling me exactly where the error was, and how to fix it.

Refactoring

Lastly, refactoring becomes a breeze in a soundly-typed language. In JavaScript, refactoring is treacherous unless you have a solid test suite. With Reason, you can just go ahead and change that utility function, reshape that object structure, or rename any variable. The compiler will point out all of the places where the code changed, and all you have to do is follow the breadcrumbs. Once your program compiles, you can feel pretty confident that it actually won’t throw any runtime errors.

I remember seeing a case study from Facebook about Messenger.com saying that after they had migrated a sizable portion of their application to Reason that the time it took to do major refactors had gone down from days to a few hours.

Note: when talking to many people about Reason, one of the common questions I’m asked is whether a solid type system can be replaced by unit and integration test coverage. My answer is mostly that it depends. You can get 100% type coverage through testing, but you’re going to spend a lot of time writing tests for edge cases (what if I pass a string as an argument? An array? An integer?). In addition, you’re likely going to need to document the types flowing through your program (something like JSDoc) to make it easier to trace. A type system won’t catch all bugs in your program and shouldn’t replace unit/integration tests (you’ll still need to test your business logic). However, it can help with testing all the edge cases and you’ll get much faster feedback about breaking changes. Give it a shot—I didn’t think I would like having the type system and I was pleasantly surprised.

Immutable and functional by default, but still supports mutation and side-effects

By default the syntax of Reason and OCaml supports purely functional paradigms.

For example, take how Reason handles functions with multiple parameters.

let myFunction = (a, b, c, d) => a + b + c + d;

Reason automatically curries functions with multiple arguments, so this function would compile to something like this:

let myFunction = a => b => c => d => a + b + c + d;

The automatic currying makes it super easy to partially apply the arguments as you go along, rather than doing some code gymnastics to make sure that you have all the data needed when you actually call the function. The function doesn’t actually get executed until the last argument is applied.

Secondly, most of the data structures and types in Reason are immutable by default. When you declare a variable via let, it’s immutable—you can’t reassign the variable or change its value. Fields in records (the equivalent of an object in Reason) can’t be changed, you have to create a new record that overwrites the field you wanted to change.

All that being said, sometimes you need to just get stuff done, and the clearest way to solve the problem at hand happens to be writing a little bit of imperative code or introducing a tad of immutability into your program. Reason allows you to declare variables as mutable, but you have to explicitly say “I want this thing to be mutable, I’m not mutating it by accident”.

Here’s what the syntax looks like:

/\* immutable variable \*/
let num = 1;
/\* mutable variable \*/
let mutableNum = ref(1);
mutableNum := 2 /\* Reassign the value of the variable \*/

Mutable record fields share a similar syntax that forces you to declare the field as mutable:

type record = { 
  a: int, 
  mutable b: int,
}

let myRecord = { a: 1, b: 2 };
myRecord.b = 4; /\* We can change b, but not a! \*/

Having our records and variables frozen by default prevents lots of accidental errors. However, having the ability to do things like mutation and imperative loops (Reason still supports for loops, you don’t need recursion for everything!) puts another tool in your tool belt.

It’s awesome that Reason / OCaml are pure by default — pure code tends to be clearer and easier to trace. However, pure programs at some point need to make a side effect: they need to write to the console, render to the DOM, or make an API call. Having the ability to write the impure side-effect code lets us write real programs that go to production. As a whole, the language feels very pragmatic—encourage pure, functional code, but allow imperative code when needed.

If you’re coming from JavaScript-land, the syntax doesn’t feel very foreign

So, sure, sound typing and functional paradigms by default are great, but is it really worth the overhead of learning a new language? Wouldn’t it be easier to just be really diligent and stick with the tools and libraries I already know?

In this case, not really. The team behind Reason has taken extra care to make the syntax friendly to both beginners to programming and people migrating from the JavaScript ecosystem. The syntax is so close that the following function is syntactically valid in both JavaScript and Reason

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

Granted, this example is really simple, but it shows that the syntax in Reason feels very much like JavaScript. To me, it feels as if you took a lot of JavaScript and cleaned up the syntax, took out classes, and added a few functional goodies into the mix (like the |> pipe syntax, although JavaScript may be getting that soon as well).

However, Reason does have some things in its syntax that will be foreign if you’re coming from JavaScript, but the Reason docs do an amazing job of explaining how these new language constructs work and how to use them effectively.

One of the coolest language features about Reason is the combination of variants and pattern matching.

A variant is a special type in Reason—it exists in other languages but if you’re coming from JavaScript it will likely be a little foreign. The closest thing to a variant type would be an enum in TypeScript.

Here’s what the variant syntax looks like:

type vehicle =
  | Car
  | Plane
  | Boat;

However, variants don’t stop there! They can carry arguments with them too, just like a function! This allows us to pass data along with our enums.

type vehicle = 
  | Car(string)
  | Plane
  | Boat;

let bmw = Car("BMW");

Even by itself, the variant type is super powerful, but the second we throw Reason’s pattern matching into the mix we’re looking at a match made in heaven.

Pattern-matching looks similar to a switch/case statement in JavaScript, with a slightly terser syntax. We can pattern-match over our variant type and spit out a string in each case (You’ll notice how we’re able to use the argument to the variant later on).

let action = switch(value) {
  /\* `++` is the Reason syntax for string concatenation \*/
  | Car(make) => "It's a " ++ make
  | Plane => "It's a plane!"
  | Boat => "It's a boat!"
}

If we forgot to handle the Boat branch of our switch statement, the compiler will throw a warning, telling us we haven’t handles all possible cases! This encourages us to handle every possible scenario or create default cases in our pattern matching.

However, the magic doesn’t just stop there. We can pattern match on pretty much any value in Reason, including arrays, integers, etc.

/\* Pattern-matching on an array \*/
switch(arr) {
  | [] => "It's empty"
  | [a] => "Only 1 item"
  | [a, b] when b == 2 => "2 items, and the 2nd is 2!"
  | \_ => "all other cases get handled here!"
}

There’s a lot of other cool goodies in the Reason syntax, so if you’re interested in taking a peek, check out this cheat sheetcomparing Reason to JavaScript.

Ok, you’ve convinced me…how do I get started?

If this article has you excited about Reason and you’re looking to get started, I’ve curated a couple links to get you up and running in no time!

First off, head on over to the Reason docs. They’re very well-written and continually being improved on, and they’ll get you acquainted with the design decisions behind the syntax, best practices, and future goals.

In addition, if you’re interested in using Reason for web development you’ll definitely want to check out the BuckleScript docs too. Lastly, if you’re looking to use ReasonReact for your React applications, here’s the tutorial and docs for that! 😀

Lastly, if you’re looking for help please don’t hesitate to tweet at me or comment on this post! You can also chime in on the Reason discord channel, the people there are very nice.

Plug: LogRocket, a DVR for web apps

https://logrocket.com/signup/

LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single page apps.

Try it for free.


Top comments (0)