Reputation: 65
var user1 = {
name: 'Nady',
active: true,
cart: [],
purchase: [],
};
var compose = function test1(f, g) {
return function test2(...args) {
return f(g(...args));
};
};
function userPurchase(...fns) {
return fns.reduce(compose);
}
userPurchase(
empty,
addItemToPurchase,
applayTax,
addItemToCart
)(user1, { name: 'laptop', price: 876 });
function addItemToCart(user, item) {
return { ...user, cart: [item] };
}
function applayTax(user) {
var { cart } = user;
var taxRate = 1.3;
var updatedCart = cart.map(function updateCartItem(item) {
return { ...item, price: item.price * taxRate };
});
return { ...user, cart: updatedCart };
}
function addItemToPurchase(user) {
return { ...user, purchase: user.cart };
}
function empty(user) {
return { ...user, cart: [] };
}
I don't understand this example well. I tried stepping through it with the debugger and concluded the following:
When I call function userPurchase
the reduce
will work and at its end f
will be test2
and g
will be addItemToCart
then test2
is returned as the accumulated. Then we call it passing (user1, { name: 'laptop', price: 876 })
as arguments and g
is called in it that is addItemToCart
.
I don't understand how g
is changed to applayTax
, then addItemToPurchase
, then empty
every time function test2
call itself.
How or why this is happening?
Upvotes: 0
Views: 630
Reputation: 50807
Part of the problem is simply in naming. It would help to introduce one more function, and to rename two others.
var composeTwo = function test1(f, g) {
return function test2(...args) {
return f(g(...args));
};
};
function composeMany(...fns) {
return fns.reduce(composeTwo);
}
const userPurchase = composeMany(
empty,
addItemToPurchase,
applyTax,
addItemToCart
)
userPurchase(user1, { name: 'laptop', price: 876 });
//=> {active: true, cart: [], name: "Nady", purchase: [{name: "laptop", price: 1138.8}]}
// other functions elided
composeTwo
(originally called compose
) is a function that accepts two functions and returns a new one which accepts some input, calls the second function with that input and then calls the first function with the result. This is straightforward mathematical composition of functions.
composeMany
(which as originally called -- very confusingly -- userPurchase
) extends this composition to work on a list of functions, using reduce
to sequentially call each function on the result of the pipeline so far, starting with the arguments passed. Note that it works from the last item in the list to the first one.
We use this to define the new userPurchase
, which passes empty
, addItemToPurchase
, applyTax
and addItemToCart
to pipeline
. This returns a function that will then apply them in sequence, doing something equivalent to function (...args) {return empty1(addItemToPurchase(applyTax(addItemToCart(...args))))}
We can see this in action in this snippet:
var user1 = {
name: 'Nady',
active: true,
cart: [],
purchase: [],
};
var composeTwo = function test1(f, g) {
return function test2(...args) {
return f(g(...args));
};
};
function composeMany (...fns) {
return fns.reduce(composeTwo);
}
const userPurchase = composeMany (
empty,
addItemToPurchase,
applyTax,
addItemToCart
)
console .log (
userPurchase(user1, { name: 'laptop', price: 876 })
)
function addItemToCart(user, item) {
return { ...user, cart: [item] };
}
function applyTax(user) {
var { cart } = user;
var taxRate = 1.3;
var updatedCart = cart.map(function updateCartItem(item) {
return { ...item, price: item.price * taxRate };
});
return { ...user, cart: updatedCart };
}
function addItemToPurchase(user) {
return { ...user, purchase: user.cart };
}
function empty(user) {
return { ...user, cart: [] };
}
.as-console-wrapper {max-height: 100% !important; top: 0}
However, I would find this much cleaner with a more modern JS syntax. This is very much equivalent, but cleaner:
const composeTwo = (f, g) => (...args) =>
f (g (...args))
const composeMany = (...fns) =>
fns .reduce (composeTwo)
// .. other functions elided
const userPurchase = composeMany (
empty,
addItemToPurchase,
applyTax,
addItemToCart
)
Here it should be obvious that composeTwo
takes two functions and returns a function. And in knowing that and understanding .reduce
, it should be clear that composeMany
takes a list of functions and returns a new function. This version, which also applies this change to the remaining functions is available in this snippet:
var composeTwo = (f, g) => (...args) =>
f (g (...args))
const composeMany = (...fns) =>
fns .reduce (composeTwo)
const addItemToCart = (user, item) =>
({ ...user, cart: [item] })
const applyTax = (user) => {
var { cart } = user;
var taxRate = 1.3;
var updatedCart = cart .map (item => ({ ...item, price: item .price * taxRate }))
return { ...user, cart: updatedCart };
}
const addItemToPurchase = (user) =>
({ ...user, purchase: user.cart })
const empty = (user) =>
({ ...user, cart: [] })
const userPurchase = composeMany (
empty,
addItemToPurchase,
applyTax,
addItemToCart
)
const user1 = {
name: 'Nady',
active: true,
cart: [],
purchase: [],
};
console .log (
userPurchase (user1, { name: 'laptop', price: 876 })
)
.as-console-wrapper {max-height: 100% !important; top: 0}
reduce
and composeTwo
work togetherHere we try to demonstrate how reduce
works with composeTwo
to compose multiple functions into one.
In the first step, the init
parameter to reduce
is missing, so JS uses the first value in the array as the initial one, and starts iterating with the second one. So reduce
first calls composeTwo
with empty
and addItemToPurchase
, yielding a function equivalent to
(...args) => empty (addItemsToPurchase (...args))
Now reduce
passes that function and applyTax
to compose
, yielding a function like
(...args) => ((...args2) => empty (addItemsToPurchase (...args2))) (applyTax (...args))
Now this is structured like the following:
(x) => ((y) => f ( g (y)) (h (x))
where x
represents ...args
, y
represents ...args2
, f
represents empty
, g
represents addItems
, and h
represents applyTax
.
but the right-hand side is a function ((y) => f ( g ( y))
) with the value h (x)
applied to it. This is the same as replacing y
in the body with h(x)
, yielding f (g (h (x)))
, so that this function is equivalent to (x) => f (g (h (x)))
, and by replacing our original values, this ends up as
(...args) => empty (addItemsToPurchase (applyTax ( ...args)))
Do note that this application of values to the function does not happen now when the function is being built. It will happen when the resulting function is called. In memory, this is still something like (...args) => ((...args2) => empty (addItems (...args2))) (applyTax (...args))
. But this logical version shows how it will work.
Of course we now do it again for addItemToCart
:
(...args) => ((...args2) => empty (addItemsToPurchase (applyTax ( ...args2)))) (addItemToCart (...args))
and by the same sort of application, we get the equivalent of
(...args) => empty (addItems (applyTax ( addItemToCart (...args))))
Which is the basic definition of the composition of those functions.
There's something strange about the hard-coded tax rate. We can fix that by making a parameter used in calling userPurchase
. This also allows us to clean up the applyTax
function:
const applyTax = (taxRate) => ({cart, ...rest}) => ({
... rest,
cart: cart .map (item => ({ ...item, price: item .price * taxRate }))
})
const userPurchase = (taxRate) => composeMany (
empty,
addItemToPurchase,
applyTax(taxRate),
addItemToCart
)
// ...
userPurchase (1.3) (user1, { name: 'laptop', price: 876 })
Note that the curried nature of this parameter lets us choose to apply just this value to get back a tax-rate specific function:
const someDistrictPurchase = userPurchase (1.12) // 12% tax
someDistrictPurchase(user, item)
Which we can see in one more snippet:
var composeTwo = (f, g) => (...args) =>
f (g (...args))
const composeMany = (...fns) =>
fns .reduce (composeTwo)
const addItemToCart = (user, item) =>
({ ...user, cart: [item] })
const applyTax = (taxRate) => ({cart, ...rest}) => ({
... rest,
cart: cart .map (item => ({ ...item, price: item .price * taxRate }))
})
const addItemToPurchase = (user) =>
({ ...user, purchase: user.cart })
const empty = (user) =>
({ ...user, cart: [] })
const userPurchase = (taxRate) => composeMany (
empty,
addItemToPurchase,
applyTax(taxRate),
addItemToCart
)
var user1 = { name: 'Nady', active: true, cart: [], purchase: []}
console .log (
userPurchase (1.3) (user1, { name: 'laptop', price: 876 })
)
.as-console-wrapper {max-height: 100% !important; top: 0}
Function composition is an essential part of functional programming (FP). While it helps to have functions like composeTwo
and composeMany
, it is much better if they have understandable names. (Note that compose
is a perfectly legitimate name for the first one. My change here was just to make the distinction with composeMany
clearer.) The biggest problem, to my mind was the original name of userPurchase
as a composition function. That confuses a lot of things.
Modern JS (arrow functions, rest/spread, and destructuring) makes for code that is not only more succinct but generally easier to understand.
Upvotes: 1
Reputation: 3371
Does this help at all?
var user1 = {
name: 'Nady',
active: true,
cart: [],
purchase: [],
};
function compose(f, g) {
const composition = function(...args) {
console.log('f name', f.name);
console.log('g name', g.name);
return f(g(...args));
};
Object.defineProperty(composition, 'name', {
value: 'composition_of_' + f.name + '_and_' + g.name,
writable: false
});
return composition;
};
function userPurchase(...fns) {
return fns.reduce(compose);
}
function addItemToCart(user, item) {
return { ...user, cart: [item] };
}
function applayTax(user) {
var { cart } = user;
var taxRate = 1.3;
var updatedCart = cart.map(function updateCartItem(item) {
return { ...item, price: item.price * taxRate };
});
return { ...user, cart: updatedCart };
}
function addItemToPurchase(user) {
return { ...user, purchase: user.cart };
}
function empty(user) {
return { ...user, cart: [] };
}
const result = userPurchase(
empty,
addItemToPurchase,
applayTax,
addItemToCart
)(user1, { name: 'laptop', price: 876 });
console.log(result);
Let's say you need to take in a number, add ten to it and then double it. You could write some functions like:
const add_ten = function(num) { return num + 10; };
const double = function(num) { return num * 2; };
const add_ten_and_double = function(num) { return double(add_ten(num)); };
You could also write the same thing as:
function compose(outer_function, inner_function) {
return function(num) {
return outer_function(inner_function(num));
};
};
const add_ten = function(num) { return num + 10; };
const double = function(num) { return num * 2; };
const add_ten_and_double = compose(double, add_ten);
console.log('typeof add_ten_and_double:', typeof add_ten_and_double);
console.log('add_ten_and_double:', add_ten_and_double(4));
Using compose, we've created a function that does the same thing as our original add_ten_and_double function. Does that make sense up to here? (Point A).
If we then decided to add five we could have:
function compose(outer_function, inner_function) {
return function(num) {
return outer_function(inner_function(num));
};
};
const add_ten = function(num) { return num + 10; };
const double = function(num) { return num * 2; };
const add_five = function(num) { return num + 5; };
const add_ten_and_double_and_add_five = compose(compose(add_five, double), add_ten);
console.log('typeof add_ten_and_double_and_add_five :', typeof add_ten_and_double_and_add_five);
console.log('add_ten_and_double_and_add_five :', add_ten_and_double_and_add_five(4));
Now we've run compose using a function and a function that was composed of two other functions, but what we've got back is still just a function that takes a number and returns a number. Does that make sense up to here? (Point B).
If we wanted to add a few more functions then we would end up with a lot of compose calls in our code, so we could just say "give me the composition of all of these functions", and it might look like:
function compose(outer_function, inner_function) {
return function(num) {
return outer_function(inner_function(num));
};
};
const add_ten = function(num) { return num + 10; };
const double = function(num) { return num * 2; };
const add_five = function(num) { return num + 5; };
functions_to_compose = [add_five, double, add_ten];
let composition;
functions_to_compose.forEach(function(to_compose) {
if(!composition)
composition = to_compose;
else
composition = compose(composition, to_compose);
});
const add_ten_and_double_and_add_five = composition;
console.log('typeof add_ten_and_double_and_add_five:', typeof add_ten_and_double_and_add_five);
console.log('add_ten_and_double_and_add_five:', add_ten_and_double_and_add_five(4));
The fns.reduce(compose); in userPurchase in your code is basically doing the same thing as the forEach loop here, but neater. Does that make sense why add_ten_and_double_and_add_five is a function that you can pass in a number and all of the operations from functions_to_compose are being applied (last to first) to the number? (Point C).
Upvotes: 1
Reputation: 113964
The thing that may have gotten you confused is taking the term accumulator
literally. By convention that's the name of the first argument to a reducer. But it's not necessary to use it to accumulate a value. In this case it is used to compose a series of functions.
The real meaning of the first argument to a reducer is previouslyReturnedValue
:
function compose(previouslyReturnedValue, g) {
return function (...args) {
return previouslyReturnedValue(g(...args));
};
}
So let's walk through this loop:
[empty, addItemToPurchase, applayTax, addItemToCart].reduce(
(f,g) => {
return (...args) => {
return f(g(...args));
}
}
);
The first round of the loop, f = empty
and g = addItemToPurchase
. This will cause compose
to return:
return (...args) => {
return empty(addItemToPurchase(...args));
}
Leaving the array to become: [applayTax, addItemToCart]
The second round of the loop f = (...args) => {return empty(addItemToPurchase(...args))}
and g = applyTax
. This will cause compose
to return:
return (...args) => {
return empty(addItemToPurchase(applyTax(...args)));
}
We continue with this logic until we finally get compose
to return the full chain of functions:
return (...args) => {
return empty(addItemToPurchase(applyTax(addItemToCart(...args))));
}
If the above is a bit hard to follow then let's name the anonymous function that becomes f
in each loop.
In the first round we get:
function f1 (...args) {
return empty(addItemToPurchase(...args));
}
In the second round we get:
function f2 (...args) {
return f1(applyTax(...args));
}
In the final round we get:
function f3 (...args) {
return f2(addItemToCart(...args));
}
It is this function f3
that is returned by reduce()
. So when you call the return value of reduce it will try to do:
f2(addItemToCart(...args))
Which will call addItemToCart()
and then call f2
which will execute:
f1(applyTax(...args))
Which will call applyTax()
and then call f1
which will execute:
empty(addItemToPurchase(...args))
Which will call addItemToPurchase()
and then call empty()
Basically all this is doing:
let tmp;
tmp = addItemToCart(args);
tmp = applyTax(tmp);
tmp = addItemToPurchase(tmp);
tmp = empty(tmp);
There is a way to implement this logic which is more readable and easier to understand if we abandon reduce()
. I personally like the functional array methods like map()
and reduce()
but this is one of those rare situations where a regular for
loop may lead to much more readable and debuggable code. Here's a simple alternative implementation that does exactly the same thing:
function userPurchase(...fns) {
return function(...args) {
let result = args;
// The original logic apply the functions in reverse:
for (let i=fns.length-1; i>=0; i--) {
let f = fns[i];
result = f(result);
}
return result;
}
}
Personally I find the implementation of userPurchase
using a for
loop much more readable than the reduce
version. It clearly loops through the functions in reverse order and keep calling the next function with the result of the previous function.
Upvotes: 2