Composing Functions in JavaScript

Christopher T.

March 12th, 2020

Share This Post

What makes JavaScript my favorite language to be writing apps with is abilities to compose so many different kinds of functions together that can eventually lead to a working program.

We see JavaScript code everywhere that demonstrates this in different ways.

Composing functions together can become extremely fun especially when they work. Unfortunately it's not always an easy thing to pull off because writing composed code has to be done without any errors or else it simply won't run.

In JavaScript, there are rules to composing things like functions together.

In this article, we will go over some examples of composing in JavaScript and talk about important concepts that always need to be kept in mind when composing.

What does it mean to compose?

Composing means to combine more than one thing to build a bigger thing. It's a general concept in mathematics where you combine two or more functions into a brand new function. Most of us have been working with this concept growing up in school, in the form of something like f(g(x)) which is pronounced "f of g of x".

In JavaScript, it can look like this:

const f = console.log
const g = (str) => `Hello, ${str}`
const sayWord = (x) => f(g(x))

sayWord('bryan') // "Hello, bryan"

Let's talk about composing functions. When we compose functions together, the main goal is to take a function and combine it with another function--so that when both of them are together gives us a more enhanced function that helps to produce a value that we want. There's multiple good reasons why people prefer to compose functions, such as reducing code and providing a more convenient reusable piece of code.

In JavaScript, functions are considered first-class meaning they can be passed around and can take on the disguise of a "value", just like strings, numbers, booleans, objects, etc. What it means is that it allows functions to take other functions as arguments and can even return functions. This is what makes JavaScript a very powerful language because you can throw them around anywhere you want.

Let's look at an example of a function that appends 'hello' to a string:

function append(str) {
  return `hello ${str}
}

It's easy to use this function and receive a string back like so:

const result = append('Gary') // 'hello Gary'

But as we just learned, functions can take functions so lets just use the second argument and test out what we can do just by adding a function argument to the function:

function append(str, modify) {
  return `hello ${str}
}

Okay, we now left an opening for the second argument to do something inside here. Just by doing this simple change it opened up for some additional functionality, like this for example:

function append(str, modify = (s) => s) {
  return `hello ${modify(str)}`
}

function capitalize(value) {
  return value.toUpperCase()
}

const result = append('boss', capitalize) // 'hello BOSS'

Because of how flexible JavaScript can be as we've seen above, learning how to compose functions in JavaScript is one of most important skills to pick up when developing JavaScript apps!

Why is it important to compose?

As mentioned earlier, there are multiple good reasons to why people compose functions.

Let's look at this scenario:

function doubleTheNums(obj) {
  const keys = Object.keys(obj)
  for (let index = 0; index < keys.length; index++) {
    const key = keys[index]
    const innerObj = obj[key]
    const innerObjKeys = Object.keys(innerObj)
    for (let innerIndex = 0; innerIndex < innerObjKeys.length; innerIndex++) {
      const innerObjKey = innerObjKeys[innerIndex]
      const innerObjKeyValue = innerObj[innerObjKey]
      if (typeof innerObjKeyValue === 'number') {
        innerObj[innerObjKey] = innerObj[innerObjKey] * 2
      }
    }
  }
  return obj
}

const results = {
  game1: {
    lakers: 40,
    celtics: 40,
    overtime: {
      lakers: 48,
      celtics: 58,
    },
  },
  game2: {
    lakers: 40,
    celtics: 21,
  },
  game3: {
    lakers: 12,
    celtics: 29,
  },
}

console.log(doubleTheNums(results))

result:

{
  "game1": {
    "lakers": 80,
    "celtics": 80,
    "overtime": {
      "lakers": 48,
      "celtics": 58
    }
  },
  "game2": {
    "lakers": 80,
    "celtics": 42
  },
  "game3": {
    "lakers": 24,
    "celtics": 58
  }
}

The doubleTheNums function is responsible for looking at an object and doubling its number value if its a number type. Why should we turn this into a composed function instead? Lets look at some problems that the current function is having first:

  1. If the object passed in was deepy nested the code will just keep awkwardly being pushed down right like a tree shape. But ain't no one got time for that kinda thing.
  2. If the object passed in was deeply nested we unnecessarily lose valuable brain energy running out of ideas to name the inner variables (innerObjKey, innerObjKeyValue might lead to deeplyInnerObjKey, deeplyInnerObjKeyValue, innerInnerInnerObjKey, innerInnerInnerObjKeyValue, etc)
  3. The code gets repetitive over time. This might confuse us and no one wants to be confused.
  4. File size is increasing

Thank the heavens we can easily just throw functions around and call it a day:

function doubleTheNums(obj) {
  const keys = Object.keys(obj)
  for (let index = 0; index < keys.length; index++) {
    const key = keys[index]
    const value = obj[key]
    if (typeof value === 'number') {
      obj[key] = obj[key] * 2
    } else if (value && typeof value === 'object') {
      doubleTheNums(obj[key])
    }
  }
  return obj
}

const results = {
  game1: {
    lakers: 40,
    celtics: 40,
    overtime: {
      lakers: 48,
      celtics: 58,
    },
  },
  game2: {
    lakers: 40,
    celtics: 21,
  },
  game3: {
    lakers: 12,
    celtics: 29,
  },
}

console.log(doubleTheNums(results))

result:

{
  "game1": {
    "lakers": 80,
    "celtics": 80,
    "overtime": {
      "lakers": 96,
      "celtics": 116
    }
  },
  "game2": {
    "lakers": 80,
    "celtics": 42
  },
  "game3": {
    "lakers": 24,
    "celtics": 58
  }
}

The technique we just used was called recursion which is a very useful technique that solves all the problems we layed out previously.

The point is, that the fact that we can use functions so freely in JavaScript makes composing functions a very important topic! However, if you're new to programming then it's worth noting here that composing functions to create more enhanced, complex ones is a common utility in any programming language. But we'll focus on JavaScript because obviously this post is about JavaScript.

If you've looked at source codes of JavaScript libraries then you've probably been exposed to a good amount of code examples that do really well in composing functions. You might have also realized that the majority of these composed functions are composed of much smaller, modular functions.

Lets take a look at a function that does nothing:

function doNothing(obj) {
  return obj ? obj : obj
}

const result = doNothing({ name: 'Bob' })

We can create a compose function that takes a bunch of functions to create one more function where it can be used the same way:

const compose = (...fns) => (arg) =>
  fns.reduceRight((acc, fn) => (fn ? fn(acc) : acc), arg)

Now we can take a bunch of useless functions at once while still keeping the same functionality!

function doNothing(obj) {
  return obj ? obj : obj
}

function doSomethingButNothingStill(obj) {
  if (obj) {
    obj = obj
  }
  return obj ? obj : obj
}

const evaluateSomething = compose(
  doSomethingButNothingStill,
  doNothing,
)

const result = evaluateSomething({
  name: 'Bob',
  lastName: 'Lopez
})

In a real world scenario it's useful because you might need to take multiple functions that have the same signature to produce a final value. If multiple functions have the same signature and they're all going to get called through the same value for example then that's when it's a good time to use the composer:

const compose = (...fns) => (arg) =>
  fns.reduceRight((acc, fn) => (fn ? fn(acc) : acc), arg)

const add = (num1) => (num2) => num1 + num2
const multiply = (num1) => (num2) => num1 * num2
const subtract = (num1) => (num2) => num1 - num2

const composedOperations = compose(add(5), multiply(2), subtract(3))

const compute = (arr, initialNum = 0) =>
  arr.reduce((acc, val) => composedOperations(acc), initialNum)

console.log(compute([-10, 25, 55, 22], 6))

In the last example, if we needed to use several math operations we decided to compose multiple math operator functions to produce the final value. This made it a little more convenient since ultimately we would still have ended up passing the same argument to the functions as we loop through the array of numbers.

Injecting callbacks to be used in conjunction with existing ones

The possibilities of good uses for composing functions are endless. But to wrap this article up we'll go over another useful use case so that you can have a better understanding of how functions can be composed to achieve a variety of scenarios.

We'll be looking at a scenario for a react app next.

Imagine we're building a button component. It's going to have an onClick event listener so that when users click on it it will receive the event and alert the name of the currentTarget element that was clicked:

import React from 'react'

function MyButton({ children, ...rest }) {
  return (
    <button onClick={(e) => window.alert(e.currentTarget.name)} {...rest}>
      {children}
    </button>
  )
}

function App() {
  return (
    <div>
      <MyButton name="alerter">Alert</MyButton>
    </div>
  )
}

export default App

What if later on we want to add an additional onClick handler to the same button from outside, for additional behavior? We can easily achieve this goal by doing this:

import React from 'react'

function MyButton({ children, onClick: onClickProp, ...rest }) {
  return (
    <button
      onClick={(e) => {
        window.alert(e.currentTarget.name)
        if (onClickProp) {
          onClickProp(e)
        }
      }}
      {...rest}
    >
      {children}
    </button>
  )
}

function App() {
  function onClick(e) {
    console.log('Here is the event object', e)
  }
  return (
    <div>
      <MyButton name="alerter" onClick={onClick}>
        Alert
      </MyButton>
    </div>
  )
}

export default App

But that isn't a good solution because if we had other components that need this functionality we have to repeat this process. It also makes the handler really awkward afterwards.

A function that composes multiple onClick functions to create an enhanced onClick function is probably a better idea because you can re-use it for other event handlers like onMouseOver, onMouseLeave, etc. Here's a function we can use to solve the last problem we had:

function callAll(...fns) {
  return (...args) => fns.forEach((fn) => fn && fn(...args))
}

Now we can use it to replace our ugly solution with an elegent one:

import React from 'react'
import callAll from '../utils/callAll'

function MyButton({ children, onClick: onClickProp, ...rest }) {
  function onClick(e) {
    window.alert(e.currentTarget.name)
  }
  return (
    <button onClick={callAll(onClick, onClickProp)} {...rest}>
      {children}
    </button>
  )
}

function App() {
  function onClick(e) {
    console.log('Here is the event object', e)
  }
  return (
    <div>
      <MyButton name="alerter" onClick={onClick}>
        Alert
      </MyButton>
    </div>
  )
}

export default App

It basically achieves the same thing. But it's important to keep in mind that when you compose functions, most of the time you should be aware of the signature of the functions that you are composing!

For example in our callAll we had to ensure that none of the functions we composed had a possibility of being given a completely different value as the first argument. In our earlier example with compose, this was the case because the result of each function was passed as the first argument to the next function in the pipeline. In callAll, a .forEach ensured that each function in the pipeline receives the event object each time because by definition it doesn't return anything to the caller even if it tried to return one.


Tags


Subscribe to the Newsletter
Get continuous updates
© jsmanifest 2020