Understanding Higher-Order Functions

parenttobias

Toby Parent

Posted on December 16, 2022

Understanding Higher-Order Functions

Working on a few article ideas, and I'm realizing that quite a few of them are dealing with callbacks, or higher-order functions. Might make sense to talk about what they are, how they work, what they mean, and the superpowers with which they can endow us.

The What

When we talk about higher-order functions, we're simply talking about a function that either takes in another function (as a parameter), or gives back another function (as a return value), or both.

An example of a function that takes in a function might be:

document.querySelector("#container").addEventListener(
  "click",
  function(event){
    console.log(`You clicked on ${event.target.dataset.name}!`);
  }
)
Enter fullscreen mode Exit fullscreen mode

We are telling the addEventListener function about two parameters: the first is a string, and the second is a function. The Event API will take that function in and, when that event occurs on that element, it will call the function for us.

An example of a function that returns a function might be this one:

const addTwoThings = function(first){
  return function(second){
    // and here, we have both first and second variables!
      return first + second;
  }
}

const add7 = addTwoThings(7);
const sayHello = addTwoThings("Hello, ");

// those are both now references to functions,
//   and those functions are scoped to unique instances of
//   addTwoThings. Let's see how we can use 'em:
console.log(sayHello("Bert")); // "Hello, Bert"
console.log(add7(28)); // 35

// Let's just complicate the crap out of things:
console.log(sayHello( add7( add7(28) ) ) ); // "Hello, 42"
Enter fullscreen mode Exit fullscreen mode

So we can pass functions in, and we can pass functions back. And this can be very powerful.

The Why

Why is this significant? Consider the ES6 array methods: filter, map, forEach, reduce, some, every for example. Each of them take in a function, and the function's signature that we are passing in is very similar for each of them. The interface is pretty uniform, which makes remembering how they work a bit easier.

A common lesson in online courses might have us write a map function, simulating the way map works in our own implementation. So I don't want to give away the answer to that one, but let's try making our own implementation of filter, and see how that might work.

First, we need to understand exactly what .filter() is doing, step by step. It is looping over the array, and testing each element in that array with a given function. The function takes in up to three parameters, the element, its index, and the original array reference, and returns a truthy or falsy value. If that returned value is truthy, we persist that element. If it is not, we don't. And then we return a new array, containing just the elements that met our function.

Incidentally, a function that returns a true/false value is often referred to as a predicate function, as in "the decision was made predicated on the outcome of this thing."

We want our myFilter to work the same way, so we would be able to call it like this:

// filter out every other element:
[1,2,3,4,5,6,7,8,9].myFilter((_, index)=>index%2===0);
// [1,3,5,7,9]
Enter fullscreen mode Exit fullscreen mode

So we'll start by defining a new Array method (which isn't best practice, but for the purpose of this test I'll just... do... bad things.)

Array.prototype.myFilter = function(/* some stuff here */){
  // and do some stuff here.
}
Enter fullscreen mode Exit fullscreen mode

First thing to note, I used a function(){...} here, and not a ()=>{...} (a traditional function, vs a lambda or fat-arrow function). I did so, because within the function this will refer to the array itself. We need a valid this, so we can't use a lambda here.

So we know we need a starting empty array, and we need a loop:

Array.prototype.myFilter = function(/* some stuff here */){
  let stuffWeWant = [];
  for(let i=0; i<this.length; i++){
    // some kind of testing?
  }
  return stuffWeWant;
}
Enter fullscreen mode Exit fullscreen mode

Now, if we refer back to how we call it, we want to be able to pass in a function. And inside the loop, we need to call that function:

Array.prototype.myFilter = function( aFilterFunction ){
  let stuffWeWant = [];
  for(let i=0; i<this.length; i++){
    if( aFilterFunction(/* but what should go here? */) ){
      // if that returns true, we'll be in here.
    }
  }
  return stuffWeWant;
}
Enter fullscreen mode Exit fullscreen mode

What do we want to pass into the function? We've told the user that the function's parameters will be the current element, the current index and the array reference:

Array.prototype.myFilter = function( aFilterFunction ){
  let stuffWeWant = [];
  for(let i=0; i<this.length; i++){
    const currentElement = this[i];
    const index = i;
    const array = this;
    if( aFilterFunction(currentElement, index, array) ){
      // if that returns true, we'll be in here.
    }
  }
  return stuffWeWant;
}
Enter fullscreen mode Exit fullscreen mode

We could have simply defined those in place, if we prefer:

Array.prototype.myFilter = function( aFilterFunction ){
  let stuffWeWant = [];
  for(let i=0; i<this.length; i++){
    if( aFilterFunction(this[i], i, this) ){
      // if that returns true, we'll be in here.
    }
  }
  return stuffWeWant;
}
Enter fullscreen mode Exit fullscreen mode

And get the same result. So now we're testing with that aFilterFunction, so if we get a truthy value back, we will simply add this[i] to our stuffWeWant array!

Array.prototype.myFilter = function( aFilterFunction ){
  let stuffWeWant = [];
  for(let i=0; i<this.length; i++){
    if( aFilterFunction(this[i], i, this) ){
      stuffWeWant.push(this[i]);
    }
  }
  return stuffWeWant;
}
Enter fullscreen mode Exit fullscreen mode

And with that, we have a working myFilter function, that looks and acts exactly the same as the ES6 filter.

Meanwhile, Back On The Farm...

Yeah, that was a bit of a sidetrack, but it does have a point - in our myFilter, we are receiving a function. And that function is being run for us, by the myFilter function. It is calling the function just as we would, passing in the expected parameters.

We are running the passed-in function by proxy. We don't need to know its name, we don't need to know where it came from, we just know it's being passed in as a parameter so we have a reference, and it will take our defined parameters or it will fail.

So it's "higher-order" - we are passing in a function and we are delegating the execution of that function to myFilter. It's a neat idea, and it seems pretty simple, but it opens up a LOT of possibilities.

Looking Ahead

This was a little bit of a sidestep from the last article, but it is a lead-in to the next one - so make sure you come back for that!

πŸ’– πŸ’ͺ πŸ™… 🚩
parenttobias
Toby Parent

Posted on December 16, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related