In this example, there is no real difference in the output. However, let's make it even simpler:
function add(a) {
return function(b) {
return a+b;
}
}
console.log(add(1)(2));
function add(a, b) {
return a+b;
}
console.log(add(1, 2));
For simplicity, the function takes two parameters. One time you have to pass them both when invoking, the other you have to pass them one by one. Let's look at how that could be useful:
function add(a) {
return function(b) {
return a+b;
}
}
let numbers = [1, 2, 3, 4, 5, 6];
//lets add the same number to all members of the array:
const numberToAdd = 5;
//traditionally
const newArray = []; //make a new array
for (let num of numbers) { //define a loop
newArray.push(num + numberToAdd); //populate the array
}
console.log(newArray);
//functional
const mappedArray = numbers.map(num => num + numberToAdd); //using .map to handle everything
console.log(mappedArray);
//using the function we have
const betterMappedArray = numbers.map(add(numberToAdd));
console.log(betterMappedArray);
So, the functional approach via .map
is shorter and easier but can be even further improved by passing a function. Without the add
function you still pass a function, you define a new addition function every time you want to just add something to an array. If you want to add variable amounts, then without add
, you have to create two new functions that fundamentally all do the same thing
function add(a) {
return function(b) {
return a+b;
}
}
let numbers = [1, 2, 3, 4, 5, 6];
console.log("using .map() with new functions");
console.log(
numbers
.map(num => num + 5)
.map(num => num + 10)
);
console.log("using .map() with add()");
console.log(
numbers
.map(add(5))
.map(add(10))
);
As you can see, put side-by-side it seems quite wasteful to make new functions every time. Even having the signature as add(a, b)
so it takes two arguments means you would have to call numbers.map(num => add(num, 5))
which does not improve anything.
Bear in mind that add
is really simple - you might have a function that is more complex. Still, going with the simple example, let's re-write the example to show how it could be useful:
function add(a) {
return function(b) {
return a+b;
}
}
const shoppingCart = [
{name: "item1", price: 1},
{name: "item2", price: 2},
{name: "item3", price: 3},
{name: "item4", price: 4},
{name: "item5", price: 5},
{name: "item6", price: 6},
];
const applyHandlingTax = add(5);
const applyShippingTax = add(10);
const numbers = shoppingCart.map(item => item.price); //extract prices from the shopping cart
const finalPrices = numbers
.map(applyHandlingTax)
.map(applyShippingTax);
console.log(finalPrices);
It's a naive example with exaggerated numbers but it just serves to show what we can do here. This is functionally the same as the previous snippet but as you can see, we now have business logic with barely any changes.
- We've defined
applyHandlingTax
as adding 5
. We've also defined applyShippingTax
to be adding 10
.
- Both of these are functions, so we can apply them via
.map
directly onto the result set we have, reducing the amount of code we have to write.
- The business rule is easy to understand, as you are writing pretty human readable code - you need very little knowledge of programming to understand
numbers.map(applyHandlingTax).map(applyShippingTax)
is applying shipping and handling tax to each of the numbers. And as a developer, it's clear that you are adding a shipping and a handling tax.
- If we are sure that
add
works correctly, then by definition anything deriving from it would also work - there is no need to test applyShippingTax
. In fact, there is nothing to test - there is no body of the function, we haven't written any logic for it, we only use the result of a function for it.
- the only meaningful code we've written is for defining
add
and to extract the prices from the items. Even then, the latter can easily be abstracted in the same way that we've done with add
, so we can apply it against any sort of result set
function extract(name) {
return function(item) {
return item[name];
}
}
const shoppingCart = [
{name: "item1", price: 1},
{name: "item2", price: 2},
{name: "item3", price: 3},
{name: "item4", price: 4},
{name: "item5", price: 5},
{name: "item6", price: 6},
];
const people = [
{name: "Alice", drives: "Audi"},
{name: "Bob", drives: "BMW"},
{name: "Carol", drives: "Citroen"},
{name: "David", drives: "Dodge"},
{name: "Esther", drives: "EDAG"},
{name: "Fred", drives: "Ford"},
];
const prices = shoppingCart.map(extract('price'));
console.log(prices);
const names = people.map(extract('name'));
const cars = people.map(extract('drives'));
console.log(names);
console.log(cars);
Even with a pretty trivial example of add
we can get pretty far. The particular style of how add
is written is known as currying - instead of taking X amount of parameters at once, the function returns another function that takes X - 1 until it's satisfied.
Functions that work with other functions - either producing them (like add
does), consuming them, or both are called higher-order functions. If we use those and currying consistently we get into the territory of functional programming. Writing in a completely functional style is not a requirement to get benefit from these - as we saw with add
, you can write a simple application without using lots of code even if we are applying business rules.