如何在JavaScript中以函数式的方式“组合”函数?

6

我正在学习函数式编程,想知道是否有一种方法可以像这样“组合”函数:

function triple(x) {
    return x * 3;
}
function plusOne(x) {
    return x + 1;
}
function isZero(x) {
    return x === 0;
}
combine(1); //1
combine(triple)(triple)(plusOne)(1); // 10
combine(plusOne)(triple)(isZero)(-1); // true

如果该段落是一个函数,则将该函数“合并”到自身中,否则将返回最终结果。谢谢!

@u_mulder 参数我猜。 - daremachine
问题在于“combines”函数的具体含义不清楚。 - PaulShovan
以下代码有效,因为它返回一个函数... x = function(){return function(b){return b*2}}; x()(2) == 4 ...虽然这不是你想要的。 - rafaelcastrocouto
5个回答

40

遗产

这是数学中称为函数复合的概念。

       f(x) = y
       g(y) = z

    g(f(x)) = z

   (g•f)(x) = z

最后一行读作"g 的 f 的 x 等于 z"。组合函数的好处在于消除了中间点,注意到在g(f(x)) = z中,我们输入一个x并得到一个z输出。这跳过了中间点y

组合是创建高阶函数并保持代码简洁的好方法。很明显,我们希望在 JavaScript 程序中使用它。


comp

JavaScript是一种具有丰富函数支持的多范式语言。我们可以创建一个简单的comp函数,将两个输入函数gf组合在一起,结果是一个函数 -

function triple(x) {
  return x * 3
}

function plusOne(x) {
  return x + 1
}

function comp(g, f) {
  return function(x) {
    return g(f(x))        // "g of f of x"
  }
}

const myfunc =
  comp(triple, plusOne)

console.log(myfunc(1))

评估

triple(plusOne(1))
triple(2)
6

组合函数

正如问题所示,我们可能需要组合不止两个函数。下面我们编写 compose 函数,它使用上面定义的简单 comp 函数将所有输入函数进行 reduce 操作。如果没有给出任何函数,则返回空函数 identity

const triple = (x) =>
  x * 3

const plusOne = (x) =>
  x + 1

const comp = (g, f) =>
  x => g(f(x))                     // "g of f of x"

const identity = (x) =>
  x

const compose = (...all) =>
  all.reduce(comp, identity)

const myfunc =
  compose(triple, triple, plusOne) // any amount of funcs

console.log(myfunc(1))

评估
triple(triple(plusOne(1)))
triple(triple(2))
triple(6)
18

管道

您可以尽情发挥创意。下面,我们编写管道,使我们的程序能够以舒适的从左到右的方向读取 -

const triple = (x) =>
  x * 3

const plusOne = (x) =>
  x + 1

const pipe = x =>
  f => pipe(f(x))

pipe(1)(plusOne)(triple)(triple)(console.log)           // 18
pipe(3)(triple)(plusOne)(triple)(plusOne)(console.log)  // 31

表达式一的评估 -

f => pipe(f(1))
pipe(plusOne(1))
f => pipe(f(2))
pipe(triple(2))
f => pipe(f(6))
pipe(triple(6))
f => pipe(f(18))
pipe(console.log(18))
18

另外一个表达式 -

f => pipe(f(3))
pipe(triple(3))
f => pipe(f(9))
pipe(plusOne(9))
f => pipe(f(10))
pipe(triple(10))
f => pipe(f(30))
pipe(plusOne(31))
f => pipe(f(31))
pipe(console.log(31))
31

相关技术

柯里化函数部分应用是与函数组合相契合的概念。上面的pipe另一个问答中被介绍为$并在这里重新演示 -

const $ = x =>           // "pipe", or whatever name you pick
  k => $ (k (x))
  
const add = x => y =>    // curried add
  x + y

const mult = x => y =>   // curried mult
  x * y
  
$ (1)                    // 1
  (add (2))              // + 2 = 3
  (mult (6))             // * 6 = 18
  (console.log)          // 18
  
$ (7)                    // 7
  (add (1))              // + 1 = 8
  (mult (8))             // * 8 = 64
  (mult (2))             // * 2 = 128
  (mult (2))             // * 2 = 256
  (console.log)          // 256


2
太好了!我花了很长时间阅读它,它对我帮助很大。感谢你的出色工作! - user3016883
@user3016883,我发现了几个错别字并进行了修正。如果你遇到了问题,希望这些修改能够帮到你 ^.^ - Mulan
我很难看出如何使用它们,如果你想做一些与将它们写入控制台之外的值。pipe(1)(plusOne)(triple)(triple)(console.log) 正确地将 18 打印到控制台,但是 z=pipe(1)(plusOne)(triple)(triple) 并没有将值 18 分配给 z,实际上它将一个函数分配给了 z。如何在变量中捕获最终结果以供以后使用? - John Smith
如果你想要“做某事”,console.log只是一个例子,你可以用任何其他执行其他操作的函数来替换它。像直接样式一样分配给变量并不是编写程序的唯一方式。如果你真的必须获取数据“出来”,你可以修改pipe来检查f == null或其他标记,并有条件地返回x - Mulan
显示剩余2条评论

1
function triple(x) {
    return x * 3;
}
function plusOne(x) {
    return x + 1;
}
function isZero(x) {
    return x === 0;
}

var combine = function (v) {
    var fn = [];
    function _f(v) {
        if (typeof v === 'function') {
            fn.push(v);
            return _f;
        } else {
            return fn.reduce(function (x, f) { return f(x); }, v);
        }
    }
    return _f(v);
};

var a, b;
console.log(combine(1)); //1
console.log(combine(triple)(triple)(plusOne)(1)); // 10
console.log(combine(plusOne)(triple)(isZero)(-1)); // true
console.log(a = combine(plusOne)); // function ...
console.log(b = a(triple)); // function ...
console.log(b(5)); // 18
console.log(combine(triple)(plusOne)(triple)(plusOne)(triple)(plusOne)(1)); // 40
// @naomik's examples
var f = combine(triple); 
var g = combine(triple)(triple); 
console.log(f(1)); // 3
console.log(g(1)); // 9 (not 6 as you stated)

创建两个函数:var f = combine(triple); var g = combine(triple)(triple); 调用 f(1); // 27,然后调用 g(1) // 1。显然,f(1) // should be 3g(1) // should be 6。这会失败,因为您依赖用户在创建新链之前完成链。问题在于 combine.fn 具有状态,并且对 combine 的后续调用将导致其他函数的 fn 状态被改变。 - Mulan
我知道这个问题,但你期望combine(triple)(triple);的结果是什么?调用总是以一个值结束,所以规则是函数*值(bfn)。 - Nina Scholz
我期望 combine(triple)(triple) 为我创建一个新的函数,我可以将其分配给变量,在稍后的时间调用它,或将其作为值传递给另一个函数。就像我上面展示的 fg 一样。 - Mulan
我已经取消了我的踩,因为你提供了一个真正有效的答案。如果坚持使用这种类型的接口,请参见此代码片段以获取更实用的实现。我在那里附上了一些其他的注释。 ^.^ - Mulan
让我们在聊天中继续这个讨论 - Nina Scholz
显示剩余2条评论

1

函数合成已经在其他答案中详细介绍,主要参考https://dev59.com/TYrda4cB1Zd3GeqPQsby#30198265,因此我的建议是直接回答您最新的问题:

如果para是一个函数,则将该函数“合并”到自身中,否则将返回最终结果。谢谢!

const chain = (g, f = x => x) => 
  typeof g === 'function'
  ? (y) => chain(y, (x) => g(f(x)))
  : f(g);

// ====

const triple = x => x * 3;
const inc = x => x + 1;
const isZero = x => x === 0;

console.log(
  chain(inc)(triple)(isZero)(-1),
);


做得好,与OP的提议完美匹配。请注意,类型检查输入和为函数提供特殊行为意味着chain不能再接受函数作为参数。例如,我们经常需要计算一个函数,比如thunk或continuation,并将其作为参数传递给另一个函数。在这种设置中,chain会危险地组合功能,而不是将其作为参数传递给组合。 - Mulan

0
在JavaScript中,通过组合简单函数可以构建复杂功能。从某种意义上说,组合是函数的嵌套,将一个函数的结果作为下一个函数的输入传递进去。但是,我们不会创建难以理解的大量嵌套,而是创建一个高阶函数compose(),它接受要组合的所有函数,并返回一个新函数供我们在应用程序中使用。
function triple(x) {
  return x * 3;
}
function plusOne(x) {
  return x + 1;
}
function isZero(x) {
  return x === 0;
}

const compose = (...fns) => x =>
  fns.reduce((acc, cur) => {
    return cur(acc);
  }, x);

const withCompose = compose(triple, triple, isZero);
console.log(withCompose(1));

组合应该是从右到左,你写成了管道。 - Hitmands

-1

您可以直接在返回值上调用函数,例如:

plusOne(triple(triple(1))) // 10
isZero(triple(plusOne(-1))) // true

请问为什么我的答案被评为差评,你能解释一下吗? - Spencer Wieczorek
2
我没有给你的帖子投反对票,但它并没有真正回答问题。OP想要以编程方式构建函数的“链”或“管道”。这被称为函数组合,而JavaScript没有内置的函数来实现它。你的答案可能会告诉OP他/她已经知道如何做了。 - Mulan
@naomik 我认为这取决于 OP 对“组合”的理解,如果 OP 已经知道了这个解决方案,我想他们会评论的。我只是简单地陈述了你可以通过执行 f(g(x)) 来获得正确的结果,而不是创建一个函数 F = f(g(x)),假设 OP 可能是初学者,也许不知道这一点。它确实(在某种程度上)结合了函数,并确实产生了 OP 想要的正确结果,所以我不明白为什么它不是一个答案。 - Spencer Wieczorek
@naomik 虽然我可能误解了问题,但显然你的答案是 OP 寻找的。 - Spencer Wieczorek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接