Skip to content

Instantly share code, notes, and snippets.

@rolandpeelen
Last active April 22, 2020 16:07
Show Gist options
  • Select an option

  • Save rolandpeelen/45487c6228bd93434da743a9c604209e to your computer and use it in GitHub Desktop.

Select an option

Save rolandpeelen/45487c6228bd93434da743a9c604209e to your computer and use it in GitHub Desktop.

ReasonML 101

In our last post, we went over some of the basic concepts in cryptography. Over the course of the following few sections in this post, we'll do a deep-dive into ReasonML. Not only to explain what it is, but also dive into some of its properties, slightly more advanced topics and why it could be an awesome option for your next Javascript project.

Introduction

Over the course of the past few years there have been two notably big trends in developing with Javascript. The first being that we're realizing dynamic languages are not ideal persé. While there is a good place for them, generally speaking, when applications get larger and more interconnected, they tend to become brittle. There are ways of safeguarding this using tests, but we're only testing for the faults we know of and can easily introduce new bugs because we haven't tested a particular use-case. Even the old 100% coverage doesn't help with that as it merely states 100% of the code has been touched. It doesn't test for bad code or whether all possible outcomes have been tested exhaustively. Please note that we're not advocating against testing here. We're simply stating that we feel that, in a Javascript (and even Typescript) environment, it's not enough to guard for bugs. The second big movement we've been seeing is a stride towards a more declarative, functional style of programming. We don't loop, we don't do if statements. Instead, we map, filter, reduce, or pattern match our way out of trouble. This, combined with some pure functions giving us referential transparancy means that our code is easier to reason about and shows intent, instead of giving instructions.

Let's start off with a note on Typed Languages. Reasoning about code in a dynamically typed language can be hard. We can create functions for numbers, but when you can dynamically cast strings to numbers, then technically your function also works with strings. Unfortunately, "nebula" isn't a number. This means it's up to the developer to be extremely strict in the use of those functions and safeguard any unwanted input. This seems like a silly example. But others aren't so trivial. The recent addition of the 'null coalescing operator' says it all (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator). If a value or object is dynamic, and you can't be certain it is what it says it is or contains what it says it contains, then every single function or value needs to do input validation before doing its operation. More often that not, this does not happen because of the old 'this thing I'm accessing should never not be here'.

Typescript

Statically typed languages can help with this. Over the recent years some transpile-to and compile-to Javascript languages have emerged. The most popular one being Typescript. It has a c# esque code style and is a superset of Javascript, meaning all Javascript is still valid Typescript. This has some advantages, as you can easily set it up into an existing codebase, slowly migrating your Javascript to Typescript without touching the existing code at all. This also has some disadvantages. The main one being that unless you go into 'strict' mode, not everything needs to be typed. And as Typescript doesn't have the best type inference system, we don't know the types of everything in the application, and that brings us right back to the start; we don't know what's what. The worst part is that even if you do all of that right and everything is types. None of your npm modules, or the ones your npm modules need, need to. And as such, we can never have full type-safety. Resorting back to a boatload of unit tests to try and keep things safe. This is one of the reasons we didn't go for Typescript. We feel that unless everything is 100% typed, the overhead of adding type annotations in Typescript is not worth the reward.

ReasonML

While there are other options, the most appealing second contenter was ReasonML. Contrary to other functional, strictly typed languages like Purescript or Elm, ReasonML's syntax is especially designed to be more familiar to Javascript users.

let double = a => a + a;

Js or ReasonML?

There are a couple of reasons (...) for us to opt for ReasonML

Compiler

There is a slight difference between Typescript and ReasonML in that compiling Typescript to Javascript keeps the same level of abstraction (so it's actually transpiling). It checks what it can check, then removes the typing information and that' it. ReasonML does a bit more. ReasonML compiles your Reason code to optimized Javascript. So while we're using map, filter, reduce and things like the option or result monad, Reason compiles this to sound Javascript that is optimized for performance. The Reason documentation states that all of the produced Javascript is quite readable. We've found that to a certain extent it is, but the more complicated the Reason code gets, the less readable the JS output. You might think all of this compilation comes at a price of speed. However, the opposite is true. I've personally seen Typescript projects (not even that big) that had compile times of over 5 minutes (running up to 30 minutes in CI for a fully tested production build). If you have to offload your compiling to a remote machine, something is not right... With ReasonML, we’re consistently seeing compile / build times of ~100ms. According to the website of the compiler (https://bucklescript.github.io/docs/en/build-performance#extreme-test), an extreme test of 10k files, with a fully clean build, took less than 3 minutes.

Hindly-Milner Type System

Since ReasonML is not a superset of JS, but a different syntax to OCaml, we’re actually underneath the surface, writing OCaml and using its type inference system. Unlike the Typescript compiler, which has to allow for plain JS to get through, ReasonML's system doesn't (unless very explicitly said so, and properly annotated with types). As such, we can infer types much better and have good assumptions about them at compile time. This means that even in really complex code situations, we hardly ever have to annotate types. This, in turn, results in much cleaner and better readable code. In general, if the code compiles, the developer has been quite strict when it comes to optional values, and your logic is sound, it runs and there will be little bugs other than ‘logic’ bugs. There is a really good readup on the workings here.

Types and Pattern Matching

Pattern Matching can be seen as sort of a switch statement on steroids and it's a great way to make decisions in your code. Let's look at an example from the typescript website on discriminant union types and their switch statement based decision process, and see how we would do this in ReasonML.

interface Square {
    kind: "square";
    size: number;
}
interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
interface Circle {
    kind: "circle";
    radius: number;
}
type Shape = Square | Rectangle | Circle;

function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
}

We can see there are some things that are not ideal.

  1. The kind is a property of the Interface, stated as a string (not a type at all).
  2. The area function is annotated with the input of Shape while we're clearly switching against properties only found in the interfaces associated with it (that annotation should not be necessary).
  3. There is a lot of repetition. Since it's very hard to deconstruct the item directly, as we don't know its properties, the code resorts to a single digit variable to keep things readable / typeable as it needs to be there seven times. We also need to type return three times (yes, I'm nitpicking now).

Let's look at how we would tackle such a thing in ReasonML.

type size = float;
type width = float;
type height = float;
type radius = float;
type shape = 
  | Square(size)
  | Rectangle(width, height)
  | Circle(radius);

let area = shape => switch(shape) {
  | Square(size) => size *. size
  | Rectangle(width, height) => width *. height
  | Circle(radius) => Js.Math._PI *. (radius ** 2);
};

The first thing to notice is that we use much less lines, 14 vs 22. If we would remove the type aliases or use a record inside the shape, this would be 10. Less than 50%. The main reason for including the type aliases is that the compiler will give us feedback and tell us it needs a radius of type float instead of just a float. Depending how easy it is to deduce the internal type for developers, one could opt to do that.

The second thing to notice is that we don't need those kind properties as we can pattern match on actual variant types. This means we can directly encode the value inside the type using type constructors. This in turn get's translated to the bucklescript compiler as an array based value, as accessing a value at an index on arrays is ever so slightly faster than accessing a value on objects. The brilliant part however is the pattern matching itself. In the typescript version we're matching on a property of an interface and checking it against strings (prone to failure / typo's). In the ReasonML version, we're pattern matching against types. The awesome part here, is that we don't have to do any further lookups, because we can 'pick out' the values from the variant type. This means that the whole thing becomes much more concise and easy to read. Another way to think about this is if the shape is a square, take the value inside that and evaluate the expression on the right side with that value being 'size'.

Finally, the following other things are different / handy to know coming from JS / TS.

  1. Since we're using floats, *. is needed as opposed to * (due to some constraints in the type system).
  2. ReasonML's nature is a pure functional language. In a pure functional language there are no side effects, and as such, every function should return something. This means that typing return every single time is rather futile. Instead, the last evaluated expression gets returned automatically.
  3. Javascripts Math library is exposed via the Js api. The _ in front of PI is done for all protected property names.
  4. Immutability is in the heart of ReasonML. As such, there is no distinction between let and const. There is just let and it is immutable like javascript's const.

Auto-Currying

Currying is the process of having a function take its arguments on at a time. Most of you that work with react will have probably find this to come in handy when using handlers in forms;

// Normal way
const handleEvent = (property, event) => setValue([ property ]: event.target.value);
<input onChange={event => handleEvent("property", event)} />

// Curried way
const handleEvent = property => value => setValue([ property ]: value);
<input onChange={handleEvent("property")} />

For those who haven't. Consider the following example;

// Normal function
const addNormal = (a,b) => a + b;
addNormal(10, 10); // 20
addNormal(10); // Nan

// Curried function
const addCurry = a => b => a + b;
addCurry(10)(10); // 20
addCurry(10); // Function ?!

const addTen = addCurry(10); // Function;
addTen(10); // 20;

What happens here is the addCurry function, instead of taking two parameters directly, takes its first one first, and then returns a new function that takes the second parameter. Written without lambdas this would look like this;

function addCurry(a) {
  return function(b) {
    return a + b;
  };
};

The main reasons for this are code conciseness (like the react example above), or to make really abstract functions more re-usable by making a new function where the first argument is already supplied. This process is called partial application. Take the example from our open-source UI-Library (https://github.com/tenzir/ui-component-library/blob/master/src/Helpers/StyleHelpers.re).

/* Add or subtract percentage of 255 from color */
let applyPercentageToColor =
    (changeType, percentage, color: Css_AtomicTypes.Color.t) => {
  let operation =
    switch (changeType) {
    | Lighten => operatePercent(percentage, Add)
    | Darken => operatePercent(percentage, Subtract)
    };
    ...
};

/* Partially applied to specifically lighten or darken a color */
let lighten = applyPercentageToColor(Lighten);
let darken = applyPercentageToColor(Darken);

In this specific case, we want to have a function to either lighten or darken a certain color by a certain percentage. We could repeat the complete applyPercentageToColor function in case we don't want to use currying, but it's much easier when we can supply the first argument and just make two new functions that are partially applied. The cool thing about ReasonML is that we don't need to specify which functions we want to be curried. All functions are auto-curried. We can supply the parameters all at once, or give them one-by-one. Whichever we like.

Infix Operators

In some languages even functions like '+' or '-' are written as prefixes (+ 6 4, - 6 5). In Javascript, these operators are infix. We can do string concattenation by "a" + "b" and do similar things with numbers. In functional languages like Haskell and also ReasonML, infix operators are more common and we can also create them ourselves. In the code we're going to talk about we'll use a couple of them extensively. So let's talk about them.

Pipe Operator (pipe first)

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

1 -> add(2) -> subtract(3) === 0;
add(1, 2) -> subtract(3) === 0;
subtract(add(1, 2), 3) === 0;
subtract(3, 3) === 0;
0 === 0;

The pipe first operator (->) pipes the value to the left (coming in), into the first argument of the function on the right.

Pipe Operator (pipe last)

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

1 |> add(2) |> subtract(3) === 0;
add(2, 1) |> subtract(3) === 0;
subtract(3, add(1, 2)) === 0;
subtract(3, 3) === 0;
0 === 0;

The pipe operator (|>) does the same as the pipe first, but instead of the left hand side going into the first argument of the next function, it goes into the last argument. There is a subtle difference there. Depending on the library used, they may favour having the first, or last argument be the 'data' argument. Generally speaking, most functions that have a couple of arguments, will only have one value that’s being worked on. Historically speaking, in OCaml, that argument was always last and thus, the |> was used. ReasonML tries to cater a bit more to JS developers, and thus came the introduction of the pipe-first -> operator (see a readup on the confusion here: https://stackoverflow.com/questions/55474593/whats-the-difference-between-and-in-reasonml).

Promise Operators The following are an answer to the somewhat cumbersome way of working with promises in ReasonML. In JS we can use async/await syntax and we have promise.then. Unfortunately, we don't have that in ReasonML. Instead, we have a Js.Promise.then_ function that takes a promise and then executes it (then is a protected keyword, so it's then_ in this case). This is quite cumbersome to write;

Js.Promise.make((~resolve, ~reject) => resolve(2))
|> Js.Promise.then_(value => doSomethingWithValue(error))
|> Js.Promise.then_(value => doSomethingWithValue(error))
|> Js.Promise.catch(error => doSomethingWithError(error))

Because this becomes quite tedious to write and a bit unreadable, we use infix operators to do this (https://github.com/digitake/bs-promise-monad).

Sidenote; While the package is called promise-monad, promises are technically not monadic because it's left hand identity function does not hold true when the argument itself is a promise (https://buzzdecafe.github.io/2018/04/10/no-promises-are-not-monads).

Anywho. Let's see it in action:

return(2)
>>= (value => somethingThatReturnsAPromise(value))
>>- (value => somethingThatReturnsAValue(value))
>>/ (error => doSomethingWithError(error))

We start off with a function that wraps our resolve, essentially creating a promise. Then there are three operators we generally use. In plain Javascript, when working with promises, whether to return a promise or a value, inside of the .then, doesn’t matter. The .then will either wrap the value into a promise, or just return the promise outright. Our infix operators make a distinction between those. The first one (>>=) is used for instances where we’re already returning a promise. This is like a monadic bind or flatMap. The second one (>>-) is used in instances where we’re not returning a promise. It can be seen as a regular map (take the value from the promise, apply a function and wrap it back into a promise). The last one we commonly use (>>/), wraps the catch function in the instance we return a regular value (not a promise). There is a fourth function (>>|), which is like the catch, but it, like the >>=, does not wrap the value into another promise.

'Proper' Monads

Monad’s are tricky and this certainly is not another monad tutorial. We do however want to illustrate some of the properties and why you might use them. Let's do this with an example. Say we are on a page in our application that displays some metadata for a user. In this specific case, we want to display the users tagline, which is inside the users profile.

type profile = {
  name: string,
  tagline: option(string),
};
type user =  {
  profile: option(profile)
};

The first thing to note in the type definition here is the use of option. In a language like Reason, there is no such thing as null. Instead, we use the optional values, that either have something in there, or nothing. The way we interact with them is quite similar to the way we interact with lists. We run functions on the things inside them using map, where the map function itself knows how to handle the type. So in case of a list, when we map over an empty list, the function knows that it shouldn't run the function, because there is nothing in there. The same thing goes for the option. We can use map, and the map function either executes the function we pass into it (when there is a value) or just returns the option type. What's important is that we always get the "wrapper" back. So when you call map on a list, you get back a list, and when you call map on an option, you get back an option. When a type settles this requirement (amongst some other slightly more theoretical properties), we can call it a Functor. We'll get to our m-word in a bit.

First, let's go back to our example. What we want to do is print a list of the query results. The first thing we need to do is to take out our queries. As it turns out, next to applying a function to the inner value using map, we can also peek inside when we pattern match and we can do something like this:

let tagline = switch(user.profile) {
  | Some(profile) => profile.tagline
  | None => None 
};

So now we can see the internals of the option functor. But also a problem. Because we get back another option. So let's fix this.

let optionalTagline = switch(user.profile) {
  | Some(profile) => profile.tagline
  | None => None 
};
let actualTagline = switch(optionalTagline) {
  | Some(tagline) => tagline
  | None => ""
}

So now we have the string we can use. but this code is pretty darn verbose. Let's use our map function instead...

let tagline = 
  user.profile
  -> Belt.Option.map(profile => profile.tagline)
  ...

This is a problem. Because the map function gives us back an option of something, and profile.tagline is already an option. So this means that we would have an option(option(string)). That's not ideal.

What if we could combine those two options into one option though? To do this, we need to define a new sort of 'flatten' operation, in which we define what happens when we get two optional values.

None + None = None
Some(a) + None = None 
None + Some(b) = Some(b) 
Some(a) + Some(b) = Some(b)

Once we have defined a function that handles this logic for us, our Functor just leveled up and became a Monoid too!

Now, if we take our map function, and our flatten and we stick those together in, let's say a flatMap, we get our superpowers and this makes our type a Monad. Or in other, famous words; "A monad is just a monoid in the category of endofunctors, what's the problem?". In general, you can forget about the endo part, as programmers we primarily deal with endofunctors. So much so that in Haskell, a functor is actually an endofunctor...

Naturally, for the well read category theorist, there is some more too it. But for the purpose of this article, those details aren't necessary. So let's get our tagline.

let tagline = 
  user.profile
  -> Belt.Option.flatMap(profile => profile.tagline)
  -> Belt.Option.getWithDefault("");

So now our flatMap handles all magic to get us to the direct value and we have introduced a second function to get the value, defaulting to an empty string if we encounter a None. The cool thing about this is that it doesn't matter wether our profile was None, or our tagline was. It handles both cases in one go.

Conclusion

We've gone over some of the basics in both cryptography and ReasonML. So in the next blog it's time to start building.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment