ReasonML – the new way of escaping JavaScript runtime exceptions

ReasonML – the new way of escaping JavaScript runtime exceptions

screenshot-2

JavaScript is a great tool for quick prototyping and developing smallscale applications, but this speed comes at a price. Dynamic types have a huge impact on the codebase. This starts from a significant amount of manual type checking and tests to make sure that data is of the same shape we expect and ends with runtime errors. This kind of errors can even crash your app. It is pretty difficult to avoid this kind of errors when application codebase is big or you depend on dynamic data that you get from API.

Doing any kind of refactoring on the dynamic codebase is risky without a solid test suit that will help you to see what’s broken after even insignificant change. That’s why tools like TypeScript and Flow became so important in the past years. The only issue that they are too verbose. No one is safe from any type that slipped in the codebase during crunch to release app faster.

Here comes ReasonML. Reason is a functional language built by Facebook. It uses a solid base of mature and battle-tested OCaml, which has been around since the nineties. Reason uses new build tools and syntax on top of OCaml and can be compiled to the bytecode that can run on a variety of platforms as well as compile to JavaScript with the help of a tool called BuckleScript.

architecture

The diagram shows how ReasonML fits into the OCaml ecosystem

BuckleScript is a tool build by Bloomberg which transpires OCaml or Reason code into optimized JavaScript. Due to the fact that BuckleScript uses optimized data structures, code that it produces can have better performance than vanilla JavaScript out of the box. And BuckleScript compiler is fast. It optimized to do impressive things like incremental compilation in milliseconds after you modified your code and has fantastic type inference. With the help of tools that are provided by BuckleScriptReason is fully interoperable with existing JavaScript codebases and packages.

Syntax

Reason provides syntax that can be quite familiar for JavaScript developers.

Primitive Example
Strings “Reason”
Characters ‘x’
Integers 42, -42
Floats 42.0, -42.0
Integer Addition 42 + 1
Float Addition 42.0 +. 1.0
Integer Division/Multiplication 1 / 42 * 1
Float Division/Multiplication 1.0 /. 42.0 *. 1.0
Float Exponentiation 42.0 ** 42.0
String Concatenation “Hello ” ++ “World”
Comparison <, >, >=, ==<
Boolean !, &&, ||
Reference, Structural(deep) Equality ===, ==
Immutable Lists [1, 2, 3]
Arrays [|1,2,3|]
Recordstype circle = {radius: int}; {radius: 5}
Comments/* You comment */

Variables

In Reason all bindings are immutable. Once it refers to a value, it cannot refer to anything else. But you can create a binding with the same name that will shadow previous value and refer to recently assigned one.

Functions

Functions are declared in the same way arrow functions declared in JavaScript.

let sayHello = (name) => "Hello " ++ name;

If you need to declare a complex function, use curly braces as you do it in JavaScript.

let sayHelloAndWave = (name, wave) => {    wave();
"Hello " ++ name; };

The major difference is that the last expression of the function is returned by default and you can’t preemptively exit function.

Multi-argument functions, especially those whose arguments are of the same type, can be confusing to call. In OCaml/Reason, you can attach labels to an argument by prefixing the name with the ~ symbol:

let sayHello = (~name, ~wave) => {   "Hello " ++ name   ... }

Reason functions are automatically curried and can be partially called.

By default value can’t see a binding that points to it, so we need to use a rec keyword to allow functions recursively call themselves:

let rec loopInfinitely = () => loopInfinitely();

Types

All types in Reason can be inferred. That means the type of system deduces the types for you. You don’t need to manually write them down. Type coverage is always 100%. This speeds up the prototyping phase.

Additionally, tooling like language servers provided by IDEs provides autocompletion based on inferred types. The type system is completely “sound”. This means that, as long as your code compiles, you won’t be surprised that certain binding is of the type it should not be. A pure Reason application can’t have null bugs.

Variants

A variant allows us to express our relationship.

type responseVariant =   | Yes   | No   | Kinda; 
 
let areYouStillIntrested = Kinda;

And there we can use the most powerful feature of Reason, the switch expression.

A Reason’s switch may look similar to other languages’ switch (something like if/elseif/elseif…). It allows you to check every possible case of a variant. You can use it by enumerating every variant constructor of the particular variant you’d like to use, each followed by a => and the expression corresponding to that case. Yes, No and Kinda aren’t strings, nor references, nor some special data type. They’re called “constructors” (or “tag”). The | bar separates each constructor.

let message =   switch (areYouStillInterested) {   | No => "No worries. Keep going!"   | Yes => "Great!"   | Kinda => "It'll get better!"   }; /* message is "It'll get better!" */

A variant’s constructors can hold extra data.

type account =   | None   | User(string)   | Admin(string, int);

Null, Undefined & Option

Reason don’t have null nor undefined. And this is great because it removes out an entire category of bugs. No more undefined is not a function, and cannot access foo of undefined!

But we can’t get rid of the real world’s concept of ‘nonexistence’. In Reason such cases handled by option type:

type option('a) = None | Some('a)

It’s presented as a container around value. A value of type option is either None (nothing) or that actual value wrapped in a Some”.

Here’s an actual value:

let plateNumber = 777

To represent the possibility of value been a null, we can wrap the value in the option.

let plateNumber =   if (hasACar) {     Some(777);   } else {     None;   };

But what if we need to handle both cases?

switch (plateNumber) { | None => print_endline("The person doesn't have a car") | Some(number) =>   print_endline("The person's plate number is " ++ string_of_int(number)) };

By wrapping the value into the option, we’re forcing checks for that value nulls across our codebase. And as a bonus if we forgot to handle the case from our variant, on compile time we will get a warning that one of the cases is not handled:

Warning 8: this pattern-matching is not exhaustive. Here is an example of a value that is not matched: None

With the help of pattern matching can produce extremely concise, compiler-verified, performant code.

Modules

By default, every .re file is visible as a module in the global scope of the app, but you can additionally create modules inside modules with their private bindings and nested modules.

Because files are modules, file names should, by convention, be capitalized so they match their module names. Uncapitalized file names are not invalid, but will be transformed into a capitalized module name.

To create a module, use the module keyword. The module name must start with a capital letter. Whatever you could place in a .re file, you may place inside a module definition’s {} block.

module Residents = {  type role = User | Admin; 
 let person1 = User;  let getRole = (person) =>    switch (person) {    | User => "A User"    | Admin => "An Admin"    }; };

You can access the module’s contents (including types!) using the . notation.

It can be tedious to refer constantly to a value/type in a module. Instead, we can “open” a module and refer to its contents without always prepending them with the module’s name. Instead of writing:

let p: Residents.role = Residents.getRole(Residents.person1);

We can write:

open Residents; let p: role = getRole(person1);

Each module has a type is too. It’s called a “signature”, and can be written explicitly. If a module is a .re (implementation) file, then a module’s signature is a .rei (interface) file.

/* From a previous section's example */ module type ResidenceType = {   type role;   let getRole: profession => string; };

If we don’t declare the interface for a module it will expose all its values by default. So interfaces can serve as an encapsulation mechanism.

External

external, or “FFI” (foreign function interface), or simply “interop” (for “interoperability”) is how Reason communicates with other languages, like C or JavaScript.

[@bs.val] external getElementsByClassName: string => array(Dom.element) =   "document.getElementsByClassName";

(The above is a BuckleScript-specific external that binds to a JavaScript function of the same name.) Externals can only be at the top level, or inside a module definition. You can’t declare them in e.g. a function body.

With the help of FFI we can gradually convert existing untyped JavaScript code from our project.

Conclusion

  • Reason is still a bleeding edge technology, but lack of pure Reason libraries is compensated by with access to JavaScript or OCaml ecosystems.
  • Refactoring of the Reason project is a breeze. After a change compiler will point out all the places that are affected and the moment compiler stop throw errors at you, you can be sure that you won’t get a runtime exception.
  • Reason is backed by Facebook which has been slowly converting it’s Messenger to Reason. From last Facebook’s case study:
    • full rebuild of the Reason project with few hundreds of files takes around two seconds, the incremental build is faster than 100ms on average.
    • messenger used to receive bugs on a daily basis and since of introduction of Reason that amount dropped to a total of 10 bugs in the Reason sections(and that’s during the whole year, not per week)
    • most of the new core functionality is developed in Reason
    • refactoring speed went from days to hours to dozens of minutes
Please, estimate my article. I did my best!
1 Star2 Stars3 Stars4 Stars5 Stars (3 votes, average: 5.00 out of 5)
Loading…

Leave a Reply

Your email address will not be published. Required fields are marked *

Share
Share