loading...

Where Did All My Methods Go? Modules in ReasonML

hagnerd profile image Matt Hagner ・5 min read

Coming from JavaScript to ReasonML there are a lot of barriers. While the syntax may look friendly at first, and the type inference is amazing requiring very little effort from you to massage it, there are a lot of symbols, and a huge mental shift needs to take place.

To help others coming from non-functional languages, and JavaScript
specifically, I want to start sharing some of the things that tripped me up when I was learning ReasonML.

The community is one of the best around and someone will always help you no matter how silly you think your question is, but there isn't a ton of content surrounding ReasonML online. Join the discord, and don't be afraid to post on the forum.


Functional programming languages tend to structure their code differently than non-functional languages. Whether or not you consider JavaScript to be an object oriented language, it still has some form of inheritance using the prototype chain, and you call methods directly on the instance of the data structure.

The JavaScript Way

An example. In JavaScript you can create an array literal like so:

const myFriends = ["Me", "Myself", "I"];

And you can use the Array methods by using the dot notation on the instance of the data structure.

const myFriends = ["Me", "Myself", "I"];

const shoutingFriends = myFriends.map(friend => friend.toUpperCase());

Under the Hood

In JavaScript when you declare a new array literal you are creating a new instance of the Array pseudo-class.

const myFriends = new Array("Me", "Myself", "I");

A naive implementation of an Array in JavaScript using pseudo-classes might look like this:

function MyArray() {
  // pretend I didn't use an actual Array in my definition
  this.value = Array.from(arguments)
  this.length = arguments.length
}

MyArray.prototype.forEach = function(cb) {
  for (let i = 0; i < this.value.length; i++) {
    cb(this.value[i], i, this.value);
  }  
}

MyArray.prototype.map = function(cb) {
  let __internal = [];

  this.forEach(function(element, index, array) {
    __internal.push(cb(element, index, array));
  });

  return __internal;
}

const myFriends = new MyArray("Me", "Myself", "I");
const shoutingFriends = myFriends.map(friend => friend.toUpperCase());

You might be used to seeing classes instead of pseudo-classes, but under the hood JavaScript classes are just syntatic sugar that results in the same thing.

Important to note that our methods have an implicit dependency on this (the instance itself). We aren't passing an array into the method. The implicit dependency on this is what allows the dot notation myFriends.map....

Our instance of MyArray doesn't actually copy over forEach, or map because that would be extremely inefficient. If our program has tens-of-thousands of arrays it would be unnecessary to copy EVERY method to each instance of the data structure. Instead JavaScript has this thing called the prototype.

So when we use myFriends.map... the engine goes "Hmmm.... 🤔 myFriends doesn't seem to have a function called map on its prototype. Perhaps its proto does." and then it checks MyArray, because when we create an instance of a (pseudo-)class JavaScript adds some information to our instance so later it knows who to inherit functions from.

Then it sees that "Ahhh. Yes! MyArray does a have function on its prototype called map", and then it uses that map function for our operation.

All of this rambling is to show you how data structures and methods are
tightly coupled in JavaScript. To map an array we use the instance of the array, and chain a function onto it like so [1,2,3].map(n => n + 1).

When you learn this as the norm, you think, "Well yeah, duh. How else would you do it?".


The ReasonML Way

In functional programming languages data structures store data. That's it. How do you do operate on data structures then? In ReasonML we place collections of functions that operate on a type into Modules. A module might know how to operate on one type only, like an Array for example.

The functions have an explicit dependency of an Array.

In ReasonML the Array module contains most functions that know how to operate on the array data structure. What are the benefits of modules? They're self-contained. They don't have to be copied or have pointers created to their 'owners'. They also avoid notions of 'this/self' or the instance of the data structure at all.

They are pure functions that take explicit arguments and return consistent results.

An Example

In ReasonML Array.map is a function that takes two arguments:

  1. A function that takes whatever the type of data that is contained inside of the array (in the following example it takes a string) and does something to transform each element returning either the same, or a different type (in following example it returns a string).
  2. The actual Array we want to operate on.
let myFriends = [| "Me", "Myself", "I" |];
let shoutingFriends = Array.map(friend => String.uppercase(friend), myFriends);

This explicit need for the Array to be passed in might still seem like an odd choice, especially if you're used to taking advantage of method chaining in JavaScript or other object oriented languages.

const myArray = [1, 2, 3, 4, 5, 6];
const res = myArray
  .map(n => n * 3)
  .filter(n => n % 2 === 0)
  .reduce((acc, n) => acc + n, 0);
// => 36

However, you can achieve the same thing in ReasonML in a few different ways.

(I am using a List here instead of an Array for reasons we'll explore in another post, but for our purposes squint and pretend it's an array).

let myArray = [1, 2, 3, 4, 5, 6];
let res = myArray
  |> List.map(n => n * 3)
  |> List.filter(n => n mod 2 === 0)
  |> List.fold_left((acc, n) => acc + n, 0);
/* => 36 */

Alternatively if you are using Bucklescript you can use Belt.

let myArray = [| 1, 2, 3, 4, 5, 6 |];
let res = myArray
  -> Belt.Array.map(n => n * 3)
  -> Belt.Array.keep(n => n mod 2 === 0)
  -> Belt.Array.reduce((acc, n) => acc + n, 0);
/* => 36 */

Or the Js.Array module...

let myArray = [| 1, 2, 3, 4, 5, 6 |];
let res = myArray
  |> Js.Array.map(n => n * 3)
  |> Js.Array.filter(n => n mod 2 === 0)
  |> Js.Array.reduce((acc, n) => acc + n, 0);
/* => 36 */

I don't want to dive into what the |> and -> operators are in this post. Just know that the |> and -> operators, in addition to currying, can help you achieve this same idea of method chaining. You're taking the result from one function and piping it into the next function.

So in ReasonML methods move from being available on the instance of a data structure, to being contained within a dedicated module. This allows them to be pure functions that require you to explicitly pass in the data structure you want to operate on.


Some Additional Info

If you're comfortable reading type signatures, the following links contain the type definitions for the Array and List modules.

Array module
Belt Array module
List module
Belt List module


What's next?

ReasonML is still evolving so there are often many ways to skin a cat, and there are often good reasons when to choose one over the other.

In future posts I'm going to explore some of the different modules included in the standard library in ReasonML, as well as Belt and how they relate to JavaScript.

Posted on by:

Discussion

markdown guide
 

Nice explanation. One thing I personally like about using the Js module is that it compiles to native JS methods.

myArray |> Js.Array.map(x => x * 3);

turns into:

myArray.map(x => x * 3);

If you use Array or Belt, then the JS output will import functions from the Bucklescript library and use those instead. I'm assuming there isn't much of a difference in the end, but why import extra functions if you don't have to?

 

That's a really good point especially if you're targeting JavaScript. I'm hoping to dive more into the differences between the modules, and what the benefits of each are. It doesn't seem like the community has fully decided on which is the defacto set of modules to use, and at first the differences seem rather small, but I've noticed there's quite a bit of nuance.