DEV Community

J David Eisenberg
J David Eisenberg

Posted on

Higher-Order Functions in ReasonML

In a previous post, I developed a recursive function that found the index of the first negative value in a list of quarterly balances:

let debitIndex = (data) => {
  let rec helper = (index) => {
    if (index == Js.Array.length(data)) {
      -1; 
    } else if (data[index] < 0.0) {
      index;
    } else {
      helper(index + 1); 
    }   
  };  

  helper(0);
};

let balances = [|563.22, 457.81, -309.73, 216.45|];
let result = debitIndex(balances);
Enter fullscreen mode Exit fullscreen mode

What if I wanted to find the first value that was 1000.00 or more? Then I’d need to write another function like this:

let goodQuarterIndex = (data) => {
  let rec helper = (index) => {
    if (index == Js.Array.length(data)) {
      -1; 
    } else if (data[index] >= 1000.0) {
      index;
    } else {
      helper(index + 1); 
    }   
  };  

  helper(0);
};
Enter fullscreen mode Exit fullscreen mode

It’s the same as the first function, except for the if test for the data value.

What about finding the first value that’s equal to zero? I’d need to write yet another function that looks exactly the same as the preceding two, except for the if test. There must be a way to write a generic findFirstIndex function that can handle any of these cases without having to duplicate most of the code.

We can do this by writing a higher-order function—a function that takes another function as its argument.

Let’s put the tests for “we found it!” (the only code that’s different) into their own functions that take an item as input and return a true or false value.

let isDebit = (item) => {item < 0.0};
let isGoodQuarter = (item) => {item >= 1000.00};
let isZero = (item) => {item == 0.0};
Enter fullscreen mode Exit fullscreen mode

These functions that return boolean are sometimes called predicate functions .

Then change the recursive function as follows:

let findFirstIndex = (testingFunction, data) => {
  let rec helper = (index) => {
    if (index == Js.Array.length(data)) {
      -1; 
    } else if (testingFunction(data[index])) {
      index;
    } else {
      helper(index + 1);
    }
  };

  helper(0);
};

Enter fullscreen mode Exit fullscreen mode

Now you can find the first negative balance, the first quarter with a good balance, and the first zero-balance quarter with these calls:

let firstDebit = findFirstIndex(isDebit, balances);
let firstGoodQuarter = findFirstIndex(isGoodQuarter, balances);
let firstZero = findFirstIndex(isZero, balances);
Enter fullscreen mode Exit fullscreen mode

When the first call is made, this is what it looks like:

diagram showing isDebit function box pointing to testingFunction and balances pointing to data

As a result, the call testingFunction(data[index]) will pass balances[index] to the isDebit() function.

The second time we call findFirstIndex, testingFunction(data[index]) will pass balances[index] to the isGoodQuarter() function.

The third time we call findFirstIndex, testingFunction(data[index]) will pass balances[index] to the isZero() function.

This ability to plug in a function as an argument to another function gives you great flexibility and helps you avoid a lot of duplicated code.

Example: Rate of Change

Not all the functions you give to a higher-order function (HOF) need to be predicate functions. Consider this graph of the function f(x) = x^2

graph of parabola with horizontal and vertical lines in range 0-2 and 4-6

In this diagram, you can see that the y value increases more slowly when x is between 0 and 2 than when x is between 4 and 6.

We want to write a function that calculates the rate of change for some function f. If you have two points x1 and x2, the formula for rate of change between those points is (f(x2) – f(x1)) / (x2 – x1).

Here’s the rate of change function:

let rateOfChange = (f, x1, x2) => {
    (f(x2) -. f(x1)) /. (x2 -. x1)
};
Enter fullscreen mode Exit fullscreen mode

We are presuming here that function f returns a floating point value. ReasonML is very strict about not mixing integer and floating values; it’s so strict that it has separate arithmetic operators for each type. To add, subtract, multiply, or divide floating point numbers, you have to follow the operator with a dot.

Now let’s define the x^2 function and find the rates of change:

let xSquared = (x) => {x *. x};

let rate1 = rateOfChange(xSquared, 0.0, 2.0);
let rate2 = rateOfChange(xSquared, 4.0, 6.0);
Js.log2("Rate of change from 0-2 is", rate1); // 2.0
Js.log2("Rate of change from 4-6 is", rate2); // 10.0
Enter fullscreen mode Exit fullscreen mode

It’s possible to find the rate of change for any sort of mathematical function:

let tripleSine = (x) => {sin(3.0 *. x)};
let polynomial = (x) => {
   5.0 *. x ** 3.0 +. 8.0 *. x ** 2.0 +. 4.0 *.x +. 27.0
};

let rate3 = rateOfChange(tripleSine, 0.0, Js.Math._PI /. 4.0);
let rate4 = rateOfChange(polynomial, -3.0, 2.0);
Js.log2("sine from 0-45 degrees:", rate3); // 0.9003...
Js.log2("polynomial from -3 to 2:", rate4); // 31
Enter fullscreen mode Exit fullscreen mode

Anonymous Functions

There’s a way to specify short functions to pass to a HOF right on the spot without having to create a new, named function (as we have done so far). Here’s how we’d do it for the debit problem:

let firstDebit = findFirstIndex( (item) => {item < 0.0}, balances);
Enter fullscreen mode Exit fullscreen mode

The anonymous predicate function is (item) => {item < 0.0}. It’s exactly the same as the right-hand side of the binding starting let isDebit =...

Similarly, we can use an anonymous function for finding rate of change of the function x^3 in the range 0 to 4:

let cubeChange = rateOfChange((x) => {x ** 3.0}, 0.0, 4.0);
Enter fullscreen mode Exit fullscreen mode

Many programmers like to use anonymous functions, and you will see a lot of them as you read other people’s ReasonML code. Should you use them too? My strong suggestion is that you don’t.

  • Named functions make things easier to read. isDebit is more immediately meaningful than having to parse (item) => {item < 0.0}
  • As you find bugs or need features, your one-line anonymous function might grow to a multi-line anonymous function. That makes the code more difficult to read, and you tend to lose the big picture. At that point, you will probably pull out the code into a separate function. So why not do it now?
  • Anonymous functions aren’t reusable. If you need the same function in a different place, you will have to copy it.

That’s my viewpoint; your mileage may vary.

Summary

Higher-order functions (HOFs) are functions that take other functions as arguments. HOFs can also return a function as their value, though we haven’t covered that aspect in this post. HOFs let you reuse code that would otherwise require writing near-duplicated code.

While you might not have need to write HOFs yourself, you will often find yourself using them. For example, when you want to manipulate lists and arrays you’ll use the map(), keep(), and reduce() HOFs. But those three functions are the subject of a future post.

Top comments (0)