Higher-Order Functions in ReasonML
J David Eisenberg
Posted on December 18, 2019
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);
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);
};
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};
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);
};
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);
When the first call is made, this is what it looks like:
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
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)
};
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
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
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);
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);
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.
Posted on December 18, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.