ReasonML - Advent of Code - Day 1

01 December 2019 — Written by Amit Solanki
#reasonml#javascript#nodejs#adventOfCode

Advent Of Code

Advent of Code, is an advent calendar to solve small programming puzzles, every day for 25 days. once you solve one puzzle the next one opens up.

Signup for Advent Of Code 2019 if you haven't already, and please try and attempt it before reading this post, especially if you are a beginner. We are going to attempt it in ReasonML

Intro to ReasonML

Reasonml is a typed language which compiles down to Javascript. To get more intro on it I would recommend you to visit the official getting started guide.

I am only going to introduce the language features which we are going to use and should be aware of.

  • To define a variable we use the let keyword followed by the variable name.
let counter = 0;
  • Types are inferred. This means that we don't have to write types for every variable we use. In the above example the variable counter is of type int.

  • If a variable is of type int (integer) we cannot do any operations which needs a float (Floating number). To do any operation using that variable we will have to type cast (convert) it into another type, which is float in this example.

let floatingCounter = float_of_int(counter);
  • Array is defined like so
let inputArray = [|1, 2, 3, 4|]
  • Notice we use | after [ and before ]. This is to differentiate between array (similar to Javascript arrays) and List which is just [1]. We will focus on array for now.
  • We define functions like so.
let myFunction = (param) => {
  // function body ...
  param
}
  • Last statement of a function is the return value.
  • If else statement is to be used for ternaries. By this I mean that the last statement in their block is the return value of the block, for example.
let initialValue = 10;
let counter =
  if (initialValue <= 10) {
    5;
  } else {
    10;
  };
  • We don't need to import any files here. Files are automatically scoped as modules in ReasonML.
  • Reasonml can be converted to Ocaml and Javascript. We use BuckleScript to convert ReasonML to Javascript, which provides us some handy utility functions from JS-land in Reason, like Map, Reduce, Filter etc. We will be using Map and Reduce from Belt which is a stdlib shipped with BuckleScript.
  • To log using console.log we can use Js.log

Setup

We want a bare minimum setup, in which we can create a file for each day and run it to get the solution. We will run the generated JS file in Node.

npm install -g bs-platform # to install buckle-script
bsb -init advent-of-code -theme basic-reason # initialize our project
cd advent-of-code
yarn add nodemon # nodemon helps us reload node server on file changes

We'll need a few scripts to get output on every change, add the following in you package.json.

"scripts": {
    "nodemon": "nodemon ./src/index.bs.js",
    "build": "bsb -make-world",
    "start": "bsb -make-world -w",
    "clean": "bsb -clean-world"
  }

The files generated by BuckleScript has .bs before them and are readable like normal JS (they don't uglify it), we can edit them but I wouldn't recommend at this stage.

There are two scripts of interest here. start and nodemon. start will generate .bs.js files for us whenever we make change in .re (reasonml) files. nodemon will restart the node process whenever index.bs.js file changes or any of it's imported files change.

Rename the Demo.re file in src to Day1.re and create a function main in it, like so.

let main = () => {
  Js.log("Hello World")
}

Lets create our point of entry file, that is the index.re which will generate index.bs.js. Create the file in src. We need to call our main function in Day1.re from index.re, in reasonml there is no need to import from other files, they are scoped with their file names so to call main function in index.re we can directly call it as Day1.main(). So our index.re file should look like.

Day1.main()

Now let's look at the first problem.

First Problem

We are given a list of masses, and need to calculate the fuel needed for each one of them and report the total amount of fuel needed. Fuel needed for each item of mass m if given by mass / 3 - 2.

To solve the given problem, we will loop through each item in the list(array) calculate the fuel values, then add them to get the final result.

Let's define some test values in an array.

let test_values = [| 14, 9, 12 |]

Now we need a function to calculate the fuel for a given mass. This function will take an int divide it 3, floor the result and subtract 2 from it.

let fuel_calculator = (mass) => {
  let mass_as_float = float_of_int(mass); // we need mass in float since floor accepts only floating values
  let mass_divided_by_three = mass_as_float /. 3.0; // notice the division sign and 3 as 3.0
  let floored_value = floor(mass_divided_by_three);
  let floored_value_as_int = int_of_float(floored_value)
  let fuel_value = floored_value_as_int - 2;
  fuel_value // the return value
}

Here we first convert the integer input to float, using float_of_int function. Then we divide by 3.0, for denoting a value as float we have to add .0 at the end. Then we floor the value using floor function, this is similar to Math.floor in JS. The floored value is a float and we want int to proceed, so we convert it to int using in_of_floor. Then we just subtract the value with 2.

let's call the function to test if it works properly.

Js.log(fuel_calculator(14)) // should log '2'

let's refactor the function a bit.

let fuel_calculator = mass => {
  int_of_float(floor(float_of_int(mass) /. 3.0)) - 2;
};

We can use the |>(pipe) operator to pipe the value from one function to another. let's see how we can use it in the above function.

let fuel_calculator = mass => {
  ((mass |> float_of_int) /. 3.0 |> floor |> int_of_float) - 2;
};

You can read the above function as take mass and convert it to float, divide by 3, floor it, convert it back to int and subtract 2 from the result.

Now that we have a way to calculate the fuel, let's look into looping over the array. Just as in javascript we use Array.map to map over an array to get another array, we can use Belt.Array to loop over an array. (I am assuming that you are aware of JS Map).

We can use it like below.

let mapping_function = (array_value) => array_value
let result = Belt.Array.map(input_array, mapping_function)

Since it would be tedious to write Belt.Array every time we use it, let's alias it.

module A = Belt.Array
let mapping_function = (array_value) => array_value
let result = A.map(input_array, mapping_function)

It would also be better if we can just pipe the array into Map like we did before. |> is used to pipe the last argument, and -> is used to pipe the first argument.

module A = Belt.Array
let mapping_function = array_value => array_value;
let result = input_array->A.map(mapping_function);

Great, now we know how to map and we have the mapping function as well as the input, let's combine all of them together

module A = Belt.Array;

let fuel_calculator = mass => {
  ((mass |> float_of_int) /. 3.0 |> floor |> int_of_float) - 2;
};

let initialValue = [|14, 9, 12|];

let main = () => {
  let result = initialValue->A.map(fuel_calculator);
  result |> Js.log;
}

Console should log [2, 1, 2]. Now we have an array of fuel values for each mass, we need to add all of them to get final result.

To do so we will use Array.reduce. and an add function. The add function takes 2 values i and j adds them and returns the value. (I am assuming you know Array.reduce from JS)

module A = Belt.Array;

let fuel_calculator = mass => {
  ((mass |> float_of_int) /. 3.0 |> floor |> int_of_float) - 2;
};

let initialValue = [|14, 9, 12|];

let add = (i, j) => i + j;

let main = () => {
  let result = initialValue->A.map(fuel_calculator)->A.reduce(add);
  result |> Js.log;
}

Console should log 5. Now you can pass the input you get from Advent Of code to get your solution.

Second Problem

Now we have to calculate the mass of the fuel we are adding, and the fuel required to add that fuel, and so on and so forth. This looks like a classic recursive call problem. let's see how we can approach it in ReasonML.

we first need to calculate the fuel required to carry the said mass, then we have to calculate the fuel required to carry that Fuel, and recursively call until the mass is 0.

let rec getTotalFuel = mass => {
  if (mass <= 0) {
    0;
  } else {
    let fuelReq = getFuel(mass);
    if (fuelReq <= 0) {
      mass;
    } else {
      getTotalFuel(fuelReq) + mass;
    };
  };
};

Notice we have added rec after let, to say that this function is a recursive function. Let's refactor it a bit, we can do away with just one if-else here. Since we know that any value less than 9 will mean fuel required is 0.

let rec get_total_fuel = mass => {
  if (mass < 9) {
    mass;
  } else {
    let fuelReq = getFuel(mass);
    get_total_fuel(fuelReq) + mass;
  };
};

The above function should give us our result, now we need to run it for each mass-fuel value.

module A = Belt.Array;

let fuel_calculator = mass => {
  ((mass |> float_of_int) /. 3.0 |> floor |> int_of_float) - 2;
};

let initialValue = [|14, 9, 12|];

let add = (i, j) => i + j;

let rec get_total_fuel = mass => {
  if (mass < 9) {
    mass;
  } else {
    let fuelReq = getFuel(mass);
    get_total_fuel(fuelReq) + mass;
  };
};

let main = () => {
  let result = initialValue->A.map(fuel_calculator)->A.map(get_total_fuel)->A.reduce(add);
  result |> Js.log;
}

Et voilà, we have our solution. We can refactor the above functions, like tail call optimization etc. We will explore more tomorrow. Message me or comment here if you have any question.

Amit Solanki