1. Function Composition

Function composition is the act of treating functions like small lego bricks that you tie together. Composing simple, well known functions to achieve complex behavior is like having and advanced vocabulary used to discuss a complex topic.

If you want to try some of this out, I recommend Ramda’s REPL for experimenting with different JavaScript features such as this.

1.1. Bespoke: Average

Here’s some code for writing an average function. It computes an average value from a list of numbers. For reference, an average is all of the numbers added together and divided by the number of values.

function average(list) {
  let total = 0
  for(let i = 0; i < list.length; ++i) {
    total = total + list[i]
  }
  const result = total / list.length
  return result
}

// Test it.
console.log(average([1, 2, 3]))
console.log(average([1, 1, 1, 1]))
console.log(average([0, 1, -1]))
console.log(average([2,2,2,2,2,1]))
2
1
0
1.8333333333333333

1.2. Composed: Average

Using function composition we can create average a little differently.

// We need to make some basic operations compoasable.
function add(x, y) { return x + y }
function divide(x, y) { return x / y }

function sum(list) {
  return list.reduce(add, 0)
}

function average(list) {
  return divide(sum(list), list.length)
}

// Test it.
console.log(average([1, 2, 3]))
console.log(average([1, 1, 1, 1]))
console.log(average([0, 1, -1]))
console.log(average([2,2,2,2,2,1]))
2
1
0
1.8333333333333333

The results are the same as our bespoke version.

The value here is not that there is necessarily less code.

Each function can be thought of independently and solved as an independent problem. Can you tell me if add is solid? What about divide? sum gets a little more tricky, but even then there’s not much to it. when we get to average we are only calling two functions and using two parameters.

When you get really comfortable with functions like map, reduce, and filter, almost any operation can be composed using some combination of those three. They become the elementary particles of programming.

1.3. Review: Functions as Data part 1

JavaScript allows functions to be assigned to variables.

We call this “functions as first-class citizens”.

Here’s how this looks:

function frobnicate(x) {
  console.log('I frobnicated ' + x.toString())
}

frobnicate(1) // Just runs frobnicate and passes it 1.

const doAThing = frobnicate

doAThing(2) // Runs frobnicate and passes it 2.
I frobnicated 1
I frobnicated 2

This isn’t particularly useful but it demonstrates that we can assign a function to a variable. In fact when we write function frobincate... we are saying frobnicate is a variable too.

1.4. Review: Functions as Data part 2

Normally when we use something like map we pass an anonymous function - meaning the function has no name. But what if we pass it a function with a name?

// Here, the body of frobnicate is written out as an anonymous function.
[1, 2, 3].map(function(x) {
  console.log('I frobnicated ' + x.toString())
})

console.log('Switching gears!')

function frobnicate(x) {
  console.log('I frobnicated ' + x.toString())
}

// Here we pass frobnicate as a variable.
[4, 5, 6].map(frobnicate)
I frobnicated 1
I frobnicated 2
I frobnicated 3
Switching gears!
I frobnicated 4
I frobnicated 5
I frobnicated 6

This is the “how” of functional composition. If we can assign a function to a variable, that means we can do anything with a function that we can do with a variable. One of the things you can do with a variable is you can pass it to another function. Computer Science dweebs call a function that accepts a function as a parameter a “higher order function”. It’s just a function that takes a function.

1.5. Call Sites in JavaScript

In order to invoke a function called foo we write foo(). The () after a symbol (name) is the indication that this is a function being called. This is how JavaScript knows this is a call site as opposed to just accessing the variable’s value.

function foo() {
  console.log('foo called')
}

foo() // Works.
foo( ) // Works.
foo () // Works.
foo(
) // Works.

const x = 1
try {
  x() // Oh noes!
} catch (e) {
  console.log('Error: ' + e)
}
foo called
foo called
foo called
foo called
Error: TypeError: x is not a function

If you try this on something that isn’t a function, you’ll see <variable> is not a function.

1.6. Writing our own Higher Order Function: Map

Let’s make our own simple higher order function. One of the utilities of higher order functions is they are inherently abstract, which kind of means it’s useless. But the function passed to the higher order function allows the higher order function to specialize while remaining abstract, and specialization is how we achieve usefulness.

In this case we will implement own version of map on Array.

map returns a new Array based on the original Array. Every element of that new Array has been transformed by a transformational function.

function map(f, originalList) {
  const newList = []
  for(let i = 0; i < originalList.length; ++i) {
    newList[i] = f(originalList[i])
  }
  return newList
}

console.log(map(function(x) { return x + 1; }, [1, 2, 3]))
function uppercase(s) {
  return s.toUpperCase()
}
console.log(map(uppercase, ['a', 'b', 'c']))
[ 2, 3, 4 ]
[ 'A', 'B', 'C' ]

Here f is the function being passed, which is just any function. So long as it obeys the arity (number of arguments) and returns something, it works.

1.7. Writing our own Higher Order Function: changeFirstLetter

function uppercase(s) {
  return s.toUpperCase()
}
function lowercase(s) {
  return s.toLowerCase()
}

// Change the first letter of ever word, using f to do the transformation.
function changeFirstLetter(s, f) {
  const words = s.split(' ') // Split words.
  const newWords = words.map(function(w) {
    const firstLetter = w[0]
    return f(firstLetter) + w.slice(1)
  })
  return newWords.join(' ') // Put the words back into a sentence.
}

console.log(changeFirstLetter('i am too lazy to capitalize.', uppercase))
console.log(changeFirstLetter('CAPSLOCK IS CRUSE CONTROL FOR AWESOME', lowercase))
// Change it up - increment the character by 1.
console.log(changeFirstLetter('I am a proper sentence actually.', function(c) {
  return String.fromCharCode(c.charCodeAt(0) + 1)
}))
I Am Too Lazy To Capitalize.
cAPSLOCK iS cRUSE cONTROL fOR aWESOME
J bm b qroper tentence bctually.

The beauty of this is changeFirstLetter doesn’t need to know about what kind of changes could be made to the first letter. That has been delegated to the function argument f.

1.8. Simplifying Promises

Promises are tricky topics in JavaScript that many engineers struggle with. Since promises operate on functions they are provided, we can use composition to simplify parts of the promise. In some ways promise chains make function composition easier to understand.

// Fake, to simplify example.
function readFile(path) {
  return JSON.stringify({
    user: 'Me',
    accounts: [
      { totalMoney: 0 },
      { totalMoney: 10000 },
      { totalMoney: 2 },
    ],
  })
}

function add(x, y) { return x + y }

function computeAccounts(payload) {
  return payload
    .accounts
    .map(x => x.totalMoney)
    .reduce(add, 0)
}

// Read an account file from a path and compute its total amount.
function accountTotal(path) {
  return Promise
    .resolve(path)
    // readFile could return a Promise instead of a concete value, and this
    // could would remain unchanged.
    .then(readFile)
    .then(JSON.parse)
    .then(computeAccounts)
}

accountTotal('foo.json').then(amount => console.log(amount))
10002

1.9. Currying to Change Arity

When doing function composition, you can use a technique called “currying” to provide different function arity (argument count). A function is said to be “curried” if it takes one argument and returns another function.

function add(x) {
  // This function just immediately returns a function.
  return function(y) {
    // The x variable is pulled in from the scope.
    return x + y
  }
}

console.log(add(1)(2))
console.log(add(5)(5))

// Then make a named function with it.
const addOne = add(1)
console.log(addOne(0))
console.log(addOne(1))

// Normally "add" has two arguments and therefore is not suitable for map. But
// with a curried add we can use it with map.
console.log([1, 2, 3].map(addOne))
// Same as above, but without the named version.
console.log([1, 2, 3].map(add(1)))
3
10
1
2
[ 2, 3, 4 ]
[ 2, 3, 4 ]

1.10. Conclusions

Function composition is a topic with a great deal of depth to it, but in an ecosystem like JavaScript you can break down almost any problem into tiny, reusable pieces. It takes some practice and starts getting easier with time as you build up a stronger vocabulary of functions for yourself.