JavaScript 中的功能组合
Functional composition in JavaScript
我知道这是很有可能的,因为我的 Haskell 朋友似乎可以在睡梦中做这种事情,但我无法理解 JS 中更复杂的函数组合。
比如说,你有这三个功能:
const round = v => Math.round(v);
const clamp = v => v < 1.3 ? 1.3 : v;
const getScore = (iteration, factor) =>
iteration < 2 ? 1 :
iteration === 2 ? 6 :
(getScore(iteration - 1, factor) * factor);
在这种情况下,假设 iteration
应该是一个整数,因此我们希望将 round()
应用于该参数。并假设 factor
必须至少为 1.3
,因此我们希望将 clamp()
应用于该参数。
如果我们将 getScore
分解成两个函数,这似乎更容易组合:
const getScore = iteration => factor =>
iteration < 2 ? 1 :
iteration === 2 ? 6 :
(getScore(iteration - 1)(factor) * factor);
执行此操作的代码可能如下所示:
const getRoundedClampedScore = compose(round, clamp, getScore);
但是撰写功能是什么样的? getRoundedClampedScore
是如何调用的?或者这是非常错误的?
compose
函数大概应该先把要组成的核心函数,用rest参数把其他函数放到一个数组里,再return 调用数组中第 i
个函数的函数,第 i
个参数:
const round = v => Math.round(v);
const clamp = v => v < 1.3 ? 1.3 : v;
const getScore = iteration => factor =>
iteration < 2 ? 1 :
iteration === 2 ? 6 :
(getScore(iteration - 1)(factor) * factor);
const compose = (fn, ...transformArgsFns) => (...args) => {
const newArgs = transformArgsFns.map((tranformArgFn, i) => tranformArgFn(args[i]));
return fn(...newArgs);
}
const getRoundedClampedScore = compose(getScore, round, clamp);
console.log(getRoundedClampedScore(1)(5))
console.log(getRoundedClampedScore(3.3)(5))
console.log(getRoundedClampedScore(3.3)(1))
我认为您遇到的部分问题是 compose
实际上不是您正在寻找的功能,而是其他功能。 compose
通过一系列函数提供一个值,而您希望预处理一系列参数,然后将这些处理过的参数提供给最终函数。
Ramda 有一个非常适合这个的效用函数,叫做 converge
。 converge
所做的是生成一个函数,该函数将一系列函数应用于一对一对应的一系列参数,然后将所有这些转换后的参数提供给另一个函数。在您的情况下,使用它看起来像这样:
var saferGetScore = R.converge(getScore, [round, clamp]);
如果您不想为了使用这个 converge
函数而涉足整个第 3 方库,您可以使用一行代码轻松定义您的。它看起来很像 CaptainPerformance 在他们的答案中使用的内容,但少了一个 ...
(你绝对不应该将其命名为 compose
,因为那是一个完全不同的概念):
const converge = (f, fs) => (...args) => f(...args.map((a, i) => fs[i](a)));
const saferGetScore = converge(getScore, [round, clamp]);
const score = saferGetScore(2.5, 0.3);
Haskell 程序员通常可以像简化数学表达式那样简化表达式。我将在这个答案中向您展示如何操作。首先,让我们看看你的表达的组成部分:
round :: Number -> Number
clamp :: Number -> Number
getScore :: Number -> Number -> Number
通过组合这三个函数,我们要创建以下函数:
getRoundedClampedScore :: Number -> Number -> Number
getRoundedClampedScore iteration factor = getScore (round iteration) (clamp factor)
我们可以将这个表达式简化如下:
getRoundedClampedScore iteration factor = getScore (round iteration) (clamp factor)
getRoundedClampedScore iteration = getScore (round iteration) . clamp
getRoundedClampedScore iteration = (getScore . round) iteration . clamp
getRoundedClampedScore iteration = (. clamp) ((getScore . round) iteration)
getRoundedClampedScore = (. clamp) . (getScore . round)
getRoundedClampedScore = (. clamp) . getScore . round
如果您想将其直接转换为 JavaScript,则可以使用反向函数组合来实现:
const pipe = f => g => x => g(f(x));
const compose2 = (f, g, h) => pipe(g)(pipe(f)(pipe(h)));
const getRoundedClampedScore = compose2(getScore, round, clamp);
// You'd call it as follows:
getRoundedClampedScore(iteration)(factor);
也就是说,最好的解决方案是简单地以有针对性的形式定义它:
const compose2 = (f, g, h) => x => y => f(g(x))(h(y));
const getRoundedClampedScore = compose2(getScore, round, clamp);
Pointfree style 通常有用,但有时毫无意义。
我知道这是很有可能的,因为我的 Haskell 朋友似乎可以在睡梦中做这种事情,但我无法理解 JS 中更复杂的函数组合。
比如说,你有这三个功能:
const round = v => Math.round(v);
const clamp = v => v < 1.3 ? 1.3 : v;
const getScore = (iteration, factor) =>
iteration < 2 ? 1 :
iteration === 2 ? 6 :
(getScore(iteration - 1, factor) * factor);
在这种情况下,假设 iteration
应该是一个整数,因此我们希望将 round()
应用于该参数。并假设 factor
必须至少为 1.3
,因此我们希望将 clamp()
应用于该参数。
如果我们将 getScore
分解成两个函数,这似乎更容易组合:
const getScore = iteration => factor =>
iteration < 2 ? 1 :
iteration === 2 ? 6 :
(getScore(iteration - 1)(factor) * factor);
执行此操作的代码可能如下所示:
const getRoundedClampedScore = compose(round, clamp, getScore);
但是撰写功能是什么样的? getRoundedClampedScore
是如何调用的?或者这是非常错误的?
compose
函数大概应该先把要组成的核心函数,用rest参数把其他函数放到一个数组里,再return 调用数组中第 i
个函数的函数,第 i
个参数:
const round = v => Math.round(v);
const clamp = v => v < 1.3 ? 1.3 : v;
const getScore = iteration => factor =>
iteration < 2 ? 1 :
iteration === 2 ? 6 :
(getScore(iteration - 1)(factor) * factor);
const compose = (fn, ...transformArgsFns) => (...args) => {
const newArgs = transformArgsFns.map((tranformArgFn, i) => tranformArgFn(args[i]));
return fn(...newArgs);
}
const getRoundedClampedScore = compose(getScore, round, clamp);
console.log(getRoundedClampedScore(1)(5))
console.log(getRoundedClampedScore(3.3)(5))
console.log(getRoundedClampedScore(3.3)(1))
我认为您遇到的部分问题是 compose
实际上不是您正在寻找的功能,而是其他功能。 compose
通过一系列函数提供一个值,而您希望预处理一系列参数,然后将这些处理过的参数提供给最终函数。
Ramda 有一个非常适合这个的效用函数,叫做 converge
。 converge
所做的是生成一个函数,该函数将一系列函数应用于一对一对应的一系列参数,然后将所有这些转换后的参数提供给另一个函数。在您的情况下,使用它看起来像这样:
var saferGetScore = R.converge(getScore, [round, clamp]);
如果您不想为了使用这个 converge
函数而涉足整个第 3 方库,您可以使用一行代码轻松定义您的。它看起来很像 CaptainPerformance 在他们的答案中使用的内容,但少了一个 ...
(你绝对不应该将其命名为 compose
,因为那是一个完全不同的概念):
const converge = (f, fs) => (...args) => f(...args.map((a, i) => fs[i](a)));
const saferGetScore = converge(getScore, [round, clamp]);
const score = saferGetScore(2.5, 0.3);
Haskell 程序员通常可以像简化数学表达式那样简化表达式。我将在这个答案中向您展示如何操作。首先,让我们看看你的表达的组成部分:
round :: Number -> Number
clamp :: Number -> Number
getScore :: Number -> Number -> Number
通过组合这三个函数,我们要创建以下函数:
getRoundedClampedScore :: Number -> Number -> Number
getRoundedClampedScore iteration factor = getScore (round iteration) (clamp factor)
我们可以将这个表达式简化如下:
getRoundedClampedScore iteration factor = getScore (round iteration) (clamp factor)
getRoundedClampedScore iteration = getScore (round iteration) . clamp
getRoundedClampedScore iteration = (getScore . round) iteration . clamp
getRoundedClampedScore iteration = (. clamp) ((getScore . round) iteration)
getRoundedClampedScore = (. clamp) . (getScore . round)
getRoundedClampedScore = (. clamp) . getScore . round
如果您想将其直接转换为 JavaScript,则可以使用反向函数组合来实现:
const pipe = f => g => x => g(f(x));
const compose2 = (f, g, h) => pipe(g)(pipe(f)(pipe(h)));
const getRoundedClampedScore = compose2(getScore, round, clamp);
// You'd call it as follows:
getRoundedClampedScore(iteration)(factor);
也就是说,最好的解决方案是简单地以有针对性的形式定义它:
const compose2 = (f, g, h) => x => y => f(g(x))(h(y));
const getRoundedClampedScore = compose2(getScore, round, clamp);
Pointfree style 通常有用,但有时毫无意义。