Functional composition looks something like this.
function first(x) {
return x + 1
}
function second(x) {
return x + 2
}
console.log(second(first(1)))
// 4
We work from inner to outer. In the example above, we call the innermost function, first(1), and pass the result of that function, 2, to the invoked second function. When we call the second function it'll look like this: second(2). Finally, when second(2) executes we get our returned value, 4. We've composed a bunch of simple functions to build more complicated ones.
Using functional composition we can break our code into smaller reusable pieces. We can then use those pieces as building blocks for creating larger functions. Each piece being a set of instructions, clearly indicating exactly how we are manipulating our data. But how can we create a compose function?
Let's build our model up in pieces. We'll look at the idea of a function as a first class citizen, and what that means in Javascript.
MDN says,
A programming language is said to have First-class functions
when functions in that language are treated like any other
variable. For example, in such a language, a function can be > passed as an argument to other functions, can be returned by > another function and can be assigned as a value to a
variable.
Two takeaways here. In order for a language to have first-class functions, functions must be able to be:
- Passed as arguments to other functions
- Returned from another function
Functions As Arguments
If you've ever used the Array map or forEach
function in Javascript you've already seen functions as arguments.
let numbers = [1, 2, 3, 4]
function square(x){
(x) => x * x
}
let squaredNumbers = numbers.map(square)
console.log(squaredNumbers)
// [1, 4, 9, 16]
The map function will call our square function on every element in the numbers array, and push the return value of our square function into a new array. Once there are no more elements to invoke our square function on, the new array is returned.
This is a simplified version of what a map function definition might look like:
function ourMap(array, fn) {
let newArray = []
for (element of array) {
newArray.push(fn(element))
}
return newArray
}
In ourMap, our passed function argument is invoked on each member of the array.
Functions As Return Values
We've seen how we use functions as arguments, but what about returning a function from a function?
It's possible!
function multiplier(x) {
return function(f) {
return x * f
}
}
let multiplyByTwo = multiplier(2)
console.log(multiplyByTwo(10))
// 20
Here the inner function knows about "x", it's within its scope, so when we call multiplier(2) we return a function that looks like this
function (f) {
return 2 * f
}
Now when we invoke multiplyByTwo, we'll invoke the function we return from our "multiplier" function. That means when we call "multiplyByTwo(10)" we get 20.
console.log(multiplyByTwo(10))
// 20
The returned function still has access to all defined variables in the closure it was created in. This is why our "multiplyByTwo" function has access to the number 2 we passed to "multiplier" when creating our "multiplyByTwo" function.
Compose Function
In order to create our compose function we're gonna want to take in any number of functions and any number of arguments to pass to each function.
This sounds a bit daunting, but luckily we can take advantage of the arguments array-like object and the Array.prototype.reduce function.
The arguments array-like object is not an array, and does
not have certain functions or properties associated with it that an array might have. That being said, you can convert it to an array using the Array.from
function if you need an array.
I'm gonna write out the entire function, so we can examine and break it down into pieces. By the end, we'll be able to compose our own understanding of a compose function!
1 function compose(...fns) {
2 return fns.reduce(
3 function reducer (accumulator, current) {
4 return function returnedFunc(...args) {
5 return accumulator(current(...args))
6 }
7 }
8 )
9 }
Let's break it down line by line.
Line 1
We declare our compose function and use the spread operator to copy all of the functions we're receiving as arguments. This is technically the arguments array-like object for our compose function, but we'll call it "fns" because those arguments will only ever be functions.
Line 2
Here we're gonna run reduce on this arguments array.
Line 3
The reduce functions takes a reducer function. Here, the "accumulator" will start at the first element in the "fns" arguments array, and the "current" will be the second.
Line 4
Here comes our returned function! The function will be returned when we invoke compose.
At this point, I think it would be helpful to see this in action.
let addAndMultiplyItself = compose(
function multiply(x) { return (x * x) },
function add(x){ return (x + x) }
)
console.log(addAndMultiplyItself)
// [Function: returnedFunc]
We've now saved our returned function into a variable and it has access to the environment in which it was defined. This means it has access to functions we passed in on line 1.
Line 5
When we call addAndMultiplyByItself, and pass in our argument(s), the reduce function will execute from innermost to outermost.
Here's the function call:
let addAndMultiplyItself = compose(
function multiply(x) { return (x * x) },
function add(x){ return (x + x) }
)
console.log(addTogetherAndMultiply(10))
Here's what happens as the reducer executes:
iteration | accumulator | current | args | returned value |
---|---|---|---|---|
1 | multiply | add | 10 | 400 |
When we invoke the function returned from compose with the argument 10, addTogetherAndMultiply(10), we run every single function compose takes as an argument on the number 10, innermost to outermost as we reduce.
Composing our functions gives us more control over adding and removing functions that may not suit a particular use case.
We can build many reusable, modular functions by following a functional composition model.
Top comments (0)