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 有一个非常适合这个的效用函数,叫做 convergeconverge 所做的是生成一个函数,该函数将一系列函数应用于一对一对应的一系列参数,然后将所有这些转换后的参数提供给另一个函数。在您的情况下,使用它看起来像这样:

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 通常有用,但有时毫无意义。