【问题标题】:Cant understand what is happening in this compose reduce example in javascript?无法理解这个 compose reduce javascript 示例中发生了什么?
【发布时间】:2021-02-18 02:35:06
【问题描述】:
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 将是test2g 将是addItemToCart 然后test2 作为累积返回。然后我们称它为传递(user1, { name: 'laptop', price: 876 }) 作为参数并在其中调用gaddItemToCart

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

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

【问题讨论】:

  • 不要被变量名accumulator 所困扰。它可以用来实现一个累加器,但在这种情况下它不是一个累加器。它是函数test2。所以f 是之前的test2。在这种情况下,f 的行为不是累加器,而是作曲家。基本上这最终构建了test2(test2(test2(test2 ...。请记住,f 是上一次迭代返回的东西,即test2
  • 不应该是 addItemToCart 然后 applayTax 然后 addItemToPurchase?
  • 对不起,我还是不明白为什么 g 会改变?在我们退出 reduce 后,函数 test2 将被调用;在那一刻,g 会是多少?为什么每次都会改变?
  • @Muhammad:请阅读closures。如果一个函数从另一个函数的调用中返回,它可以访问范围内的所有值,包括外部函数的参数及其局部变量。这对于 JS 的工作方式至关重要。
  • 我喜欢这个问题产生的各种答案!

标签: javascript recursion functional-programming reduce composition


【解决方案1】:

可能让您感到困惑的是从字面上理解术语accumulator。按照惯例,这是 reducer 的第一个参数的名称。但是没有必要使用它来积累价值。在这种情况下,它用于组合一系列函数。

reducer 第一个参数的真正含义是previouslyReturnedValue

function compose(previouslyReturnedValue, g) {
  return function (...args) {
    return previouslyReturnedValue(g(...args));
  };
}

让我们来看看这个循环:

[empty, addItemToPurchase, applayTax, addItemToCart].reduce(
    (f,g) => {
        return (...args) => {
            return f(g(...args));
        }
    }
);

循环的第一轮,f = emptyg = addItemToPurchase。这将导致compose 返回:

return (...args) => {
    return empty(addItemToPurchase(...args));
}

离开数组变成:[applayTax, addItemToCart]

第二轮循环f = (...args) => {return empty(addItemToPurchase(...args))}g = applyTax。这将导致compose 返回:

return (...args) => {
    return empty(addItemToPurchase(applyTax(...args)));
}

我们继续这个逻辑,直到我们最终得到compose 来返回完整的函数链:

return (...args) => {
    return empty(addItemToPurchase(applyTax(addItemToCart(...args))));
}

同一进程的其他视图

如果上面的内容有点难以理解,那么让我们命名在每个循环中变成 f 的匿名函数。

在第一轮我们得到:

function f1 (...args) {
    return empty(addItemToPurchase(...args));
}

第二轮我们得到:

function f2 (...args) {
    return f1(applyTax(...args));
}

在最后一轮我们得到:

function f3 (...args) {
    return f2(addItemToCart(...args));
}

reduce() 返回的正是这个函数f3。所以当你调用reduce的返回值时它会尝试做:

f2(addItemToCart(...args))

哪个会调用addItemToCart(),然后调用f2会执行:

f1(applyTax(...args))

哪个会调用applyTax(),然后调用f1会执行:

empty(addItemToPurchase(...args))

哪个会调用addItemToPurchase(),然后调用empty()

TLDR

基本上所有这些都在做:

let tmp;

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

更易读的版本

如果我们放弃reduce(),有一种方法可以实现这个逻辑,它更易读、更容易理解。我个人喜欢map()reduce() 之类的函数式数组方法,但这是常规for 循环可能导致代码更具可读性和可调试性的罕见情况之一。这是一个简单的替代实现,它做的事情完全相同:

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

我个人发现使用for 循环的userPurchase 的实现比reduce 版本更具可读性。它清楚地以相反的顺序循环遍历函数,并使用前一个函数的结果继续调用下一个函数。

【讨论】:

  • function compose(previouslyReturnedValue, g) { return function (...args) {return previouslyReturnedValue(g(...args));};} 好的,现在我明白我的想法出了什么问题。我们说fpreviouslyReturnedValue 所以它应该是:ƒ anonymous (...args) { return f(g(...args)); } 直到我们执行它然后它会定义 f 和 g 并找到 g 的最后一个值 addItemToCart() 和 f 将是相同的。我的意思是empty(addItemToPurchase(applyTax(...args))); 是回报的回报,而不是回报本身。为什么我们用它作为回报?
  • 因为像 mapreduce 这样的函数循环就是这样工作的。循环的每一轮都需要返回一个结果,而不仅仅是最后一轮。就我个人而言,我会重写代码以使其看起来像我的 TLDR 解释,因为这样更容易阅读,除非你真的需要做一些元编程——例如,如果你需要让你的用户配置需要调用的函数以什么顺序 - 假设您创建自己的 GUI 可配置 CMS。否则所有这些复杂性对我来说都是不必要的
  • @MuhammadHossam 等等.. 我有个想法让动态代码更容易阅读。让我在我的答案中添加一些内容
  • 非常感谢您的帮助。 for 循环要好得多。但我无法理解每个循环需要返回结果的含义。为什么函数不是结果?它应该在这个例子中工作吗? var x = [1, 2, 3].reduce(function test1(acc, curr) { return function test2() { return acc + curr; };}); console.log(x);
  • curr 是一个函数。您不能将+ 运算符与函数一起使用。你需要做curr(args)。正如我所提到的。它不是试图进行加法或合并操作(累加)。它正在尝试构造嵌套函数调用f3(f2(f1(g(args))))
【解决方案2】:

这有帮助吗?

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

假设您需要输入一个数字,将其加十,然后将其翻倍。您可以编写一些函数,例如:

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

你也可以这样写:

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

使用 compose,我们创建了一个与原始 add_ten_and_double 函数执行相同操作的函数。到这里为止有意义吗? (A点)。

如果我们随后决定添加五个,我们可以:

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

现在我们已经使用一个函数和一个由其他两个函数组成的函数运行了 compose,但我们得到的仍然只是一个接受一个数字并返回一个数字的函数。到这里为止有意义吗? (B点)。

如果我们想添加更多函数,那么我们最终会在代码中调用大量 compose,所以我们可以说“给我所有这些函数的组合”,它可能看起来像:

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

fns.reduce(compose);在你的代码中的 userPurchase 基本上和这里的 forEach 循环做同样的事情,但更整洁。这是否有意义,为什么 add_ten_and_double_and_add_five 是一个可以传入数字的函数,并且从 functions_to_compose 的所有操作(从最后到第一个)都应用于该数字? (C点)。

【讨论】:

  • 如果你可以帮助我更多的内部减少。在我的脑海中首先f 将是empty g 将是addItemToPurchase。之后累积将是返回值(我的问题从这里开始)返回值将是function test2(...args) {return f(g(...args)); };(不是f(g(...args)))然后第二次迭代累积将是相同的,因为返回的值将是相同的变化是仅当前值。然后执行返回的内容是 function test2 在其中 f 将是 test2g 将是发生在它身上的最后一个更改。为什么会出错
  • 我不确定我是否可以比@slebetman 已经做得更好地详细描述reduce 中发生的事情;对此感到抱歉。
  • 感谢本的大力帮助。感谢您的出色回答。
【解决方案3】:

重命名

部分问题在于命名。再引入一个函数并重命名另外两个函数会有所帮助。

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(最初称为compose)是一个函数,它接受两个函数并返回一个接受一些输入的新函数,使用该输入调用第二个函数,然后调用第一个函数结果。这是函数的简单数学组合。

composeMany(最初称为 -- 非常令人困惑 -- userPurchase)将此组合扩展到处理函数列表,使用 reduce 依次调用到目前为止管道的结果,从传递的参数开始。请注意,它从列表中的最后一项到第一项有效。

我们用它来定义新的userPurchase,它将emptyaddItemToPurchaseapplyTaxaddItemToCart传递给pipeline。这将返回一个函数,然后按顺序应用它们,执行与function (...args) {return empty1(addItemToPurchase(applyTax(addItemToCart(...args))))}

等效的操作

我们可以在这个 sn-p 中看到这一点:

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}

现代语法

但是,我会发现使用更现代的 JS 语法会更清晰。这是非常等价的,但更简洁:

const composeTwo = (f, g)  => (...args)  => 
  f (g (...args))

const composeMany = (...fns) =>
  fns .reduce (composeTwo)

// .. other functions elided

const userPurchase = composeMany (
  empty,
  addItemToPurchase,
  applyTax,
  addItemToCart
)

这里应该很明显composeTwo 接受两个函数并返回一个函数。在知道并理解.reduce 之后,应该清楚composeMany 采用函数列表并返回一个新函数。在这个 sn-p 中提供了这个版本,它也将此更改应用于其余功能:

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}

reducecomposeTwo 如何协同工作

在这里,我们尝试演示reduce 如何与composeTwo 一起使用,将多个函数组合为一个。

在第一步中,reduce 的参数init 丢失了,所以 JS 使用数组中的第一个值作为初始值,并从第二个值开始迭代。所以reduce 首先用emptyaddItemToPurchase 调用composeTwo,产生一个相当于

(...args) => empty (addItemsToPurchase (...args))

现在reduce 将该函数和applyTax 传递给compose,产生类似的函数

(...args) => ((...args2) => empty (addItemsToPurchase (...args2))) (applyTax (...args))

现在它的结构如下:

(x) => ((y) => f ( g (y)) (h (x))

其中x 代表...argsy 代表...args2f 代表emptyg 代表addItemsh 代表applyTax

但右侧是一个函数 ((y) => f ( g ( y))),其值为 h (x)。这与将正文中的y 替换为h(x) 相同,产生f (g (h (x))),因此此函数等效于(x) => f (g (h (x))),并且通过替换我们的原始值,最终结果为

(...args) => empty (addItemsToPurchase (applyTax ( ...args)))

请注意,在构建函数时,现在不会将值应用于函数。它会在调用结果函数时发生。在记忆中,这仍然类似于(...args) => ((...args2) => empty (addItems (...args2))) (applyTax (...args))。但这个合乎逻辑的版本显示了它是如何工作的。

当然,我们现在为addItemToCart 再做一次:

(...args) => ((...args2) => empty (addItemsToPurchase (applyTax ( ...args2)))) (addItemToCart (...args))

通过同样的应用,我们得到等价于

(...args) => empty (addItems (applyTax ( addItemToCart (...args))))

这些函数的组成的基本定义是什么。

清理税率

硬编码税率有点奇怪。我们可以通过在调用userPurchase 时使用一个参数来解决这个问题。这也让我们可以清理applyTax函数:

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

请注意,此参数的柯里化性质使我们可以选择仅应用此值来获取特定于税率的函数:

const someDistrictPurchase = userPurchase (1.12) // 12% tax

someDistrictPurchase(user, item)

我们可以在另一个 sn-p 中看到:

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}

课程

  • 函数组合是函数式编程 (FP) 的重要组成部分。虽然拥有composeTwocomposeMany 之类的函数会有所帮助,但如果它们具有易于理解的名称会更好。 (请注意,compose 是第一个完全合法的名称。我在这里的更改只是为了更清楚地区分 composeMany。)在我看来,最大的问题是 userPurchase 的原始名称作为组合功能。这会混淆很多事情。

  • 现代 JS(箭头函数、rest/spread 和解构)使得代码不仅更简洁,而且通常更容易理解。

【讨论】:

  • 如果你可以帮助我更多的内部减少。在我的脑海中首先f 将是emptyg 将是addItemToPurchase。之后累积将是返回值(我的问题从这里开始)返回值将是function test2(...args) {return f(g(...args)); }; (不是f(g(...args)))那么第二次迭代累积将是相同的,因为返回的值将是相同的,改变的只是当前值。然后执行返回的函数 test2 在其中 f 将是 test2g 将是发生在它身上的最后一个更改。为什么会出错
  • @Muhammad:我添加了一个部分,试图更全面地解释这一点。我使用了这些函数的现代 JS 版本,因为它们更容易用于这种描述,但同样的原则也适用于原始版本。
  • 所以javascript中的函数知道其中的每个变量到底指的是什么,甚至在它执行之前?我读过很多文章说函数执行有两个阶段,编译阶段和执行阶段。在编译阶段,引擎将看到函数中的每个变量,并将用 var 定义的变量分配给 undefined(hoisting) 但这里函数test2进入编译阶段并知道每个变量指的是什么(比如f是@987654403 @) 甚至在我们执行它之前。我的结论是即使我们没有执行该函数,编译阶段也会发生?
  • @Muhammad:恐怕我不明白你的大部分问题。您可能想阅读closures。但是对于结论性查询,是的,编译阶段无论是否执行都会发生。
  • 感谢 Scott 的大力帮助,现在我更清楚了。我认为我的问题是要了解该函数甚至在执行之前就知道其中的内容,所以我认为最后f 将是test2,当它执行时它会调用自己并且会有类似无限的东西循环。
猜你喜欢
  • 2013-08-26
  • 1970-01-01
  • 2018-11-02
  • 2014-04-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-05-12
  • 2021-07-18
相关资源
最近更新 更多