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.
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.
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|] |
Records | type circle = {radius: int}; {radius: 5} |
Comments | /* You comment */ |
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 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();
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.
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);
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.
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, 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.