JS Brain Dump { part 2 } : 'High Order Functions and Promises'

ยท

5 min read

This article is part of a series answering some of the most fundamental (and sometimes confusing) JavaScript concepts

Higher Order Functions

Higher order functions is a scary way to describe a function that takes another function as an argument. Although it can get complicated, understanding this idea (as well as the premise of functional programming) can make your life as a developer a lot easier.

Let's roll back to some JS fundamentals first.

In JavaScript, we know that functions can accept arguments in the form of primitive types and objects (read my essay here if you need clarification on these two types).

Here are two example functions:

Primitive as an argument:

function square(x) {
  return x * x;
}
square(6) // => 36

Object as an argument:

function calculatePrice(prices) {
  let sum = 0;
  for (const price of prices) {
    sum = sum + price;
  }
  return sum;
}
calculatePrice([3.99, 21.99, 4.99]); // => 30.94

Our square function is expecting a number since some basic arithmetic will executed. Our calculatePrice function will expect an array since some iteration and addition will be executed.

But could we for example pass another function as the argument for either of these? Of course! As is repeated over and over:

Functions in JavaScript are first-class citizens

Let's make the calculatePrice function a little more dynamic by allowing it to do more than simply add prices together. We can do this by changing it to a high order function.

function calculatePrice(operation, initialPrice, prices) {
  let total = initialPrice;
  for (const price of prices) {
    total = operation(total, price);
  }
  return total;
}

function sum(n1, n2) {
  return n1 + n2;
}

function subtract(n1, n2) {
  return n1 - n2;
}

So what's going on here? Our calculatePrice function now has three different parameters: operation, initialPrice, and prices.

  • operation should come in the form of another function (a.k.a a "callback function")
  • initialPrice should come in the form of a number
  • prices, we we've seen above, should come in the form of an array of numbers

Now we can simply do more than add prices together and we can start with some initial value other than zero.

Let's say I'm going to the store with $97.60 cash and I want to calculate how much I'll have after buying a couple items. I will use the subtract operation from my initial value of 97.60 and list the prices of the items in my prices array.

calculate(subtract, 97.60, [3.99, 21.99, 4.99]);   // => 66.66

Why not just use separate functions? Why use high order functions to begin with?

What makes our above function so useful is that we can simply write any other operational function and pass it as a callback function. We can multiply, square, find the average, find the median, etc. and still use the same calculatePrice function. This is important because it keeps our code dry and prevents duplication.

Promises

Since this could get complicated, I promise to keep this section brief. '

Much like the word suggest, a promise is something the code should promise to do after a certain condition, making it ideal for asynchronous code. More aptly put:

A promise is a proxy for a value that will eventually become available.

Let's see it in action with a real-world example. Say there's a book I've been dying to read and I want to see if my local bookstore is carrying it. I call them with the title and author and they say, "Yes, we have your book in stock!". They save it for me and promise to keep it until the end of the day should I want to come purchase it. I run to the store, see the book on the shelf, and make the purchase. Our promise was resolved. But say I went to the store and there was a mixup, some mistake and it's not there. Our promise was rejected. How can we code a program like this?

let bookInStock = new Promise((resolve, reject) => {
    let inStock = true;

    if(inStock) {
        // Promise is fulfilled
        resolve("We have your book in stock!");
    } else {
        // Promise is rejected
        reject("Sorry, that book is not available at the moment")
    }
});

bookInStock
    .then((response) => console.log(response))
    .catch((err) => console.log(err));

First we need to create a new promise. So we call the Promise object constructor the same way we would create any other JavaScript Object. The promise object takes in a callback function as a parameter and the callback function itself takes in two parameters: resolve and reject. Inside this callback function, we can define the conditions upon which the promise will be resolved (returning us a value) or rejected (returning us an error).

At this point, the bookstore could be making an AJAX request to the bookstore API to see if they actually have the item in stock (To make it easier, we've just gone ahead and hard-coded true for the inStock value, but a more dynamic approach is obviously possible and suggested). Then is a simple if/else statement: if it's in stock (if it's true), the we can run our resolve statement. Otherwise, our promise has been rejected and we'll call the reject function.

Now that we've made our promise, we can write the consumer functions that handle the result we get from the promise.

To do that, we call the .then method on our promise. Inside the .then we can pass a callback function as a parameter. That function runs when our promise is resolved. We have a .catch function that does the same thing, except this only runs if our promise is rejected.

Running the progam in the console returns

>>> "We have your book in stock!"

And if we were to change inStock to false:

>>> "Sorry, that book is not available at the moment"

In a later article, I'll go more in-depth about how and why this is super useful for asynchronous programming, but for now it's good to see the syntax and understand the basic use-cases of promises.

ย