首页 > 解决方案 > 无法理解这个 compose reduce javascript 示例中发生了什么?

问题描述

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: [] };
}

我不太理解这个例子。我尝试使用调试器单步执行它并得出以下结论:

当我调用函数时,userPurchase该函数将起作用,并reduce在其结束时f将作为累积返回。然后我们称它为参数传递,并在其中被调用。test2gaddItemToCarttest2(user1, { name: 'laptop', price: 876 })gaddItemToCart

我不明白如何g更改为applayTax, then addItemToPurchase, 然后empty每次函数test2调用本身。

这是如何或为什么会发生的?

标签: javascriptrecursionfunctional-programmingreducecomposition

解决方案


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))));
}

Alternate view of the same process

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()

TLDR

Basically all this is doing:

let tmp;

tmp = addItemToCart(args);
tmp = applyTax(tmp);
tmp = addItemToPurchase(tmp);
tmp = empty(tmp);

More readable version

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.


推荐阅读