如何将Javascript与currying和函数组合

时间:2017-02-10 16:19:33

标签: javascript functional-programming higher-order-functions currying function-composition

我喜欢currying,但有几个原因可以解释为什么一个Javascript开发者拒绝这种技术:

  1. 对典型咖喱图案的审美关注:f(x) (y) (z)
  2. 担心因功能调用次数增加而导致的性能损失
  3. 由于许多嵌套的匿名函数而对调试问题的担忧
  4. 关于无点风格的可读性的问题(与作曲有关的讨论)
  5. 是否有一种方法可以缓解这些问题,以便我的同事不讨厌我?

2 个答案:

答案 0 :(得分:2)

  

注意: @ftor回答了他/她自己的问题。这是答案的直接伴侣。

你已经是天才了

我认为你可能已经重新设想了partial功能 - 至少部分是这样!

const partial = (f, ...xs) => (...ys) => f(...xs, ...ys);

和它的对手,partialRight

const partialRight = (f, ...xs) => (...ys) => f(...ys, ...xs);

partial接受一个函数,一些args(xs),总是返回一个带有更多args(ys)的函数,然后应用{ {1}}至f

初步评论

这个问题的背景是关于currying和组合如何与大量的编码用户群一起发挥作用。我的评论将在同一背景下

  • 仅仅因为函数可能返回一个函数并不意味着它是 curry - 在(...xs, ...ys)上添加表示函数正在等待更多args令人困惑。回想一下currying(或部分函数应用程序)抽象arity,所以我们永远不知道函数调用何时会导致计算值或其他函数等待调用。

  • _应该翻转参数;这会给你的编码员带来一些严重的重要时刻

  • 如果我们要为curry创建包装器,reduce包装器应该保持一致 - 例如,reduceRight使用foldl但是您的f(acc, x, i)使用foldr - 这会给那些不熟悉这些选择的同事带来很多痛苦

对于下一部分,我将使用f(x, acc, i)替换您的composable,删除partial - 后缀,并修复_包装

可组合功能



foldr




程序化解决方案



const partial = (f, ...xs) => (...ys) => f(...xs, ...ys);

const partialRight = (f, ...xs) => (...ys) => f(...ys, ...xs);

const comp = (f, g) => x => f(g(x));

const foldl = (f, acc, xs) => xs.reduce(f, acc);

const drop = (xs, n) => xs.slice(n);

const add = (x, y) => x + y;

const sum = partial(foldl, add, 0);

const dropAndSum = comp(sum, partialRight(drop, 1));

console.log(
  dropAndSum([1,2,3,4]) // 9
);




更严肃的任务



const partial = (f, ...xs) => (...ys) => f(...xs, ...ys);

// restore consistent interface
const foldr = (f, acc, xs) => xs.reduceRight(f, acc);

const comp = (f,g) => x => f(g(x));

// added this for later
const flip = f => (x,y) => f(y,x);

const I = x => x;

const inc = x => x + 1;

const compn = partial(foldr, flip(comp), I);

const inc3 = compn([inc, inc, inc]);

console.log(
  inc3(0) // 3
);




部分权力!

const partial = (f, ...xs) => (...ys) => f(...xs, ...ys); const filter = (f, xs) => xs.filter(f); const comp2 = (f, g, x, y) => f(g(x, y)); const len = xs => xs.length; const odd = x => x % 2 === 1; const countWhere = f => partial(comp2, len, filter, f); const countWhereOdd = countWhere(odd); console.log( countWhereOdd([1,2,3,4,5]) // 3 );实际上可以根据需要多次应用



partial




这使它成为使用可变功能的不可或缺的工具



const partial = (f, ...xs) => (...ys) => f(...xs, ...ys)

const p = (a,b,c,d,e,f) => a + b + c + d + e + f

let f = partial(p,1,2)
let g = partial(f,3,4)
let h = partial(g,5,6)

console.log(p(1,2,3,4,5,6)) // 21
console.log(f(3,4,5,6))     // 21
console.log(g(5,6))         // 21
console.log(h())            // 21




最后,演示const partial = (f, ...xs) => (...ys) => f(...xs, ...ys) const add = (x,y) => x + y const p = (...xs) => xs.reduce(add, 0) let f = partial(p,1,1,1,1) let g = partial(f,2,2,2,2) let h = partial(g,3,3,3,3) console.log(h(4,4,4,4)) // 1 + 1 + 1 + 1 + // 2 + 2 + 2 + 2 + // 3 + 3 + 3 + 3 + // 4 + 4 + 4 + 4 => 40



partialRight




<强>摘要

好的,所以const partial = (f, ...xs) => (...ys) => f(...xs, ...ys); const partialRight = (f, ...xs) => (...ys) => f(...ys, ...xs); const p = (...xs) => console.log(...xs) const f = partialRight(p, 7, 8, 9); const g = partial(f, 1, 2, 3); const h = partial(g, 4, 5, 6); p(1, 2, 3, 4, 5, 6, 7, 8, 9) // 1 2 3 4 5 6 7 8 9 f(1, 2, 3, 4, 5, 6) // 1 2 3 4 5 6 7 8 9 g(4, 5, 6) // 1 2 3 4 5 6 7 8 9 h() // 1 2 3 4 5 6 7 8 9几乎可以替代partial,但也会解决一些额外的极端情况。让我们看看这是如何与你的初始列表相提并论的

  1. 审美问题:避免composable
  2. 表现:不确定,但我怀疑表现差不多
  3. 调试:仍然是一个问题因为f (x) (y) (z)创建新功能
  4. 可读性:我认为这里的可读性非常好。 partial足够灵活,可以在很多情况下删除积分
  5. 我同意你的观点,即完全咖喱功能无法替代。我个人发现,一旦我不再评价“丑陋”,就会很容易采用新的风格。语法 - 它只是不同,人们不喜欢不同。

答案 1 :(得分:1)

目前流行的方法规定每个多参数函数都包含在动态咖喱函数中。虽然这有助于关注#1,但其余的却没有受到影响。这是另一种方法。

可组合功能

只有在最后一个参数中才能组合一个可组合函数。为了将它们与普通的多参数函数区分开来,我用尾随下划线命名它们(命名很难)。

const comp_ = (f, g) => x => f(g(x)); // composable function

const foldl_ = (f, acc) => xs => xs.reduce((acc, x, i) => f(acc, x, i), acc);

const curry = f => y => x => f(x, y); // fully curried function

const drop = (xs, n) => xs.slice(n); // normal, multi argument function

const add = (x, y) => x + y;

const sum = foldl_(add, 0);

const dropAndSum = comp_(sum, curry(drop) (1));

console.log(
  dropAndSum([1,2,3,4]) // 9
);

除了drop之外,dropAndSum完全由多参数或可组合函数组成,但我们已经实现了与完全curried函数相同的表达能力 - 至少在这个例子中。

您可以看到每个可组合函数都需要将未处理函数或其他可组合函数作为参数。这将提高速度,特别是对于迭代功能应用。然而,一旦可组合函数的结果再次成为函数,这也是限制性的。请查看下面的countWhere示例以获取更多信息。

程序化解决方案

我们可以轻松实现程序化解决方案,而不是手动定义可组合函数:

// generic functions

const composable = f => (...args) => x => f(...args, x);

const foldr = (f, acc, xs) =>
 xs.reduceRight((acc, x, i) => f(x, acc, i), acc);

const comp_ = (f, g) => x => f(g(x));

const I = x => x;

const inc = x => x + 1;


// derived functions

const foldr_ = composable(foldr);

const compn_ = foldr_(comp_, I);

const inc3 = compn_([inc, inc, inc]);


// and run...

console.log(
  inc3(0) // 3
);

运算符函数与高阶函数

也许您注意到curry(从第一个示例开始)交换参数,而composable则没有。 curry仅适用于dropsub等运算符函数,它们分别以curried和uncurried形式具有不同的参数顺序。运算符函数是任何只需要非函数参数的函数。在这一点......

const I = x => x;
const eq = (x, y) => x === y; // are operator functions

// whereas

const A = (f, x) => f(x);
const U = f => f(f); // are not operator but a higher order functions

高阶函数(HOF)不需要交换参数,但是你会经常遇到高于2的arities,因此composbale函数很有用。

HOF是函数式编程中最棒的工具之一。它们从功能应用中抽象出来这就是我们一直使用它们的原因。

更严肃的任务

我们也可以解决更复杂的任务:

// generic functions

const composable = f => (...args) => x => f(...args, x);

const filter = (f, xs) => xs.filter(f);

const comp2 = (f, g, x, y) => f(g(x, y));

const len = xs => xs.length;

const odd = x => x % 2 === 1;


// compositions

const countWhere_ = f => composable(comp2) (len, filter, f); // (A)

const countWhereOdd = countWhere_(odd);

// and run...

console.log(
   countWhereOdd([1,2,3,4,5]) // 3
);

请注意,在A行中,我们被迫明确传递f。这是针对curried函数组合的缺点之一:有时我们需要显式传递数据。但是,如果你不喜欢无点风格,这实际上是一个优势。

结论

使函数可组合减轻了以下问题:

  1. 审美问题(较少使用咖喱图案f(x) (y) (z)
  2. 性能损失(函数调用少得多)
  3. 然而,第4点(可读性)只是略有改进(无点无风格)和第3点(调试)根本没有。

    虽然我确信完全咖喱的方法优于此处提出的方法,但我认为可组合的高阶函数值得考虑。只要您或您的同事对适当的蜷缩感到不舒服,就使用它们。