重命名
部分问题在于命名。再引入一个函数并重命名另外两个函数会有所帮助。
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,它将empty、addItemToPurchase、applyTax和addItemToCart传递给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}
reduce 和 composeTwo 如何协同工作
在这里,我们尝试演示reduce 如何与composeTwo 一起使用,将多个函数组合为一个。
在第一步中,reduce 的参数init 丢失了,所以 JS 使用数组中的第一个值作为初始值,并从第二个值开始迭代。所以reduce 首先用empty 和addItemToPurchase 调用composeTwo,产生一个相当于
(...args) => empty (addItemsToPurchase (...args))
现在reduce 将该函数和applyTax 传递给compose,产生类似的函数
(...args) => ((...args2) => empty (addItemsToPurchase (...args2))) (applyTax (...args))
现在它的结构如下:
(x) => ((y) => f ( g (y)) (h (x))
其中x 代表...args,y 代表...args2,f 代表empty,g 代表addItems,h 代表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) 的重要组成部分。虽然拥有composeTwo 和composeMany 之类的函数会有所帮助,但如果它们具有易于理解的名称会更好。 (请注意,compose 是第一个完全合法的名称。我在这里的更改只是为了更清楚地区分 composeMany。)在我看来,最大的问题是 userPurchase 的原始名称作为组合功能。这会混淆很多事情。
-
现代 JS(箭头函数、rest/spread 和解构)使得代码不仅更简洁,而且通常更容易理解。