原文: Eric Elliott - Curry and Function Compositionjavascript
译文: curry和函数组合java
提醒: 本文略长,慎重阅读git
以前看到有文章说柯里化函数,大体看了下,就是高阶函数,只是名字听起来比较高大上一点,今天逛medium
又发现了这个,看了下感受还不错,有涉及到闭包,涉及到point-free style
, 并非一股脑的安利demo
了事,这个得记录下。github
curried
函数curried
函数是个一次一个的去获取多个参数的函数。 再明白点,就是好比 给定一个带有3个参数的函数,curried
版的函数将接受一个参数并返回一个接受下一个参数的函数,该函数返回一个接受第三个参数的函数。最后一个函数返回将函数应用于其全部参数的结果。编程
看下面的例子,例如,给定两个数字,a
和b
为curried
形式,返回a
和b
的总和:数组
// add = a => b => Number
const add = a => b => a + b;
复制代码
而后就能够直接调用了:闭包
const result = add(2)(3); // => 5
复制代码
首先,函数接受a
做为参数,而后返回一个新的函数,而后将b
传递给这个新的函数,最后返回a
和b
的和。每次只传递一个参数。若是函数有更多参数,不止上面的a,b
两个参数,它能够继续像上面这样返回新的函数,直到最后结束这个函数。app
add
函数接受一个参数以后,而后在这个闭包的范围内返回固定的一部分功能。闭包就是与词法范围捆绑在一块儿的函数。在运行函数建立时建立了闭包,能够在这里了解更多。固定
的意思是说变量在闭包的绑定范围内赋值。函数
在来看看上面的代码: add
用参数2
去调用,返回一个部分应用的函数,而且固定a
为2
。咱们不是将返回值赋值给变量或以其余方式使用它,而是经过在括号中将3
传递给它来当即调用返回的函数,从而完成整个函数并返回5
。ui
部分应用程序( partial application )是一个已应用于某些但并非所有参数的函数。直白的来讲就是一个在闭包范围内固定了(不变)的一些参数的函数。具备一些参数被固定的功能被认为是部分应用的。
部分功能(partial application)能够根据须要一次使用多个或几个参数。 柯里化函数(curried function)每次返回一个一元函数: 每次携带一个参数的函数。
全部curried
函数都返回部分应用程序,但并不是全部部分应用程序都是curried
函数的结果。
对于curried
来讲,一元函数的这个要求是一个重要的特征。
point-free
风格point-free
是一种编程风格,其中函数定义不引用函数的参数。
咱们先来看看js
中函数的定义:
function foo (/* parameters are declared here*/) {
// ...
}
const foo = (/* parameters are declared here */) => // ...
const foo = function (/* parameters are declared here */) {
// ...
}
复制代码
如何在不引用所需参数的状况下在JavaScript
中定义函数?好吧,咱们不能使用function
这个关键字,咱们也不能使用箭头函数(=>
),由于它们须要声明形式参数(引用它的参数)。因此咱们须要作的就是 调用一个返回函数的函数。
建立一个函数,使用point-free
增长传递给它的任何数字。记住,咱们已经有一个名为add
的函数,它接受一个数字并返回一个部分应用(partial application)的函数,其第一个参数固定为你传入的任何内容。咱们能够用它来建立一个名为inc()
的新函数:
/ inc = n => Number
// Adds 1 to any number.
const inc = add(1);
inc(3); // => 4
复制代码
这做为一种泛化和专业化的机制变得有趣。返回的函数只是更通用的add()
函数的专用版本。咱们可使用add()
建立任意数量的专用版本:
const inc10 = add(10);
const inc20 = add(20);
inc10(3); // => 13
inc20(3); // => 23
复制代码
固然,这些都有本身的闭包范围(闭包是在函数建立时建立的 - 当调用add()
时),所以原来的inc()
继续保持工做:
inc(3) // 4
复制代码
当咱们使用函数调用add(1)
建立inc()
时,add()
中的a
参数在返回的函数内被固定为1
,该函数被赋值给inc
。
而后当咱们调用inc(3)
时,add()
中的b
参数被替换为参数值3
,而且应用程序完成,返回1
和3
之和。
全部curried
函数都是高阶函数的一种形式,它容许你为手头的特定用例建立原始函数的专用版本。
curry
curried
函数在函数组合的上下文中特别有用。
在代数中,给出了两个函数f
和g
:
f: a -> b
g: b -> c
复制代码
咱们能够将这些函数组合在一块儿建立一个新的函数(h
),h
从a
直接到c
:
//代数定义,借用`.`组合运算符
//来自Haskell
h: a -> c
h = f . g = f(g(x))
复制代码
在js
中:
const g = n => n + 1;
const f = n => n * 2;
const h = x => f(g(x));
h(20); //=> 42
复制代码
代数的定义:
f . g = f(g(x))
复制代码
能够翻译成JavaScript
:
const compose = (f, g) => f(g(x));
复制代码
但那只能一次组成两个函数。在代数中,能够写:
g . f . h
复制代码
咱们能够编写一个函数来编写任意数量的函数。换句话说,compose()
建立一个函数管道,其中一个函数的输出链接到下一个函数的输入。 这是我常常写的方法:
const compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x);
复制代码
此版本接受任意数量的函数并返回一个取初始值x
的函数,而后使用reduceRight()
在fns
中从右到左迭代每一个函数f
,而后将其依次应用于累积值y
。咱们在累加器中积累的函数,y
在此函数中是由compose()
返回的函数的返回值。
如今咱们能够像这样编写咱们的组合:
const g = n => n + 1;
const f = n => n * 2;
// replace `x => f(g(x))` with `compose(f, g)`
const h = compose(f, g);
h(20); //=> 42
复制代码
trace
)使用point-free
风格的函数组合建立了很是简洁,可读的代码,可是他不易于调试。若是要检查函数之间的值,该怎么办? trace()
是一个方便实用的函数,可让你作到这一点。它采用curried
函数的形式:
const trace = label => value => {
console.log(`${ label }: ${ value }`);
return value;
};
复制代码
如今咱们可使用这个来检查函数了:
const compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x);
const trace = label => value => {
console.log(`${ label }: ${ value }`);
return value;
};
const g = n => n + 1;
const f = n => n * 2;
/* Note: function application order is bottom-to-top: */
const h = compose(
trace('after f'),
f,
trace('after g'),
g
);
h(20);
/* after g: 21 after f: 42 */
复制代码
compose()
是一个很棒的实用程序,可是当咱们须要编写两个以上的函数时,若是咱们可以按照从上到下的顺序读取它们,这有时会很方便。咱们能够经过反转调用函数的顺序来作到这一点。还有另外一个名为pipe()
的组合实用程序,它以相反的顺序组成:
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
复制代码
如今咱们能够用pipe
把上面的重写下:
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
const trace = label => value => {
console.log(`${ label }: ${ value }`);
return value;
};
const g = n => n + 1;
const f = n => n * 2;
/* Now the function application order runs top-to-bottom: */
const h = pipe(
g,
trace('after g'),
f,
trace('after f'),
);
h(20);
/* after g: 21 after f: 42 */
复制代码
即便在函数组合的上下文以外,curry
无疑是一个有用的抽象,能够来作一些特定的事情。例如,一个curried
版本的map()
能够专门用于作许多不一样的事情:
const map = fn => mappable => mappable.map(fn);
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
const log = (...args) => console.log(...args);
const arr = [1, 2, 3, 4];
const isEven = n => n % 2 === 0;
const stripe = n => isEven(n) ? 'dark' : 'light';
const stripeAll = map(stripe);
const striped = stripeAll(arr);
log(striped);
// => ["light", "dark", "light", "dark"]
const double = n => n * 2;
const doubleAll = map(double);
const doubled = doubleAll(arr);
log(doubled);
// => [2, 4, 6, 8]
复制代码
可是,curried
函数的真正强大之处在于它们简化了函数组合。函数能够接受任意数量的输入,但只能返回单个输出。为了使函数可组合,输出类型必须与预期的输入类型对齐:
f: a => b
g: b => c
h: a => c
复制代码
若是上面的g
函数预期有两个参数,则f
的输出不会与g
的输入对齐:
f: a => b
g: (x, b) => c
h: a => c
复制代码
在这种状况下咱们如何得到x
?一般,答案是curry g
。
记住curried
函数的定义是一个函数,它经过获取第一个参数并返回一系列的函数一次获取一个参数而且每一个参数都采用下一个参数,直到收集完全部参数。
这个定义中的关键词 是“一次一个”。curry
函数对函数组合如此方便的缘由是它们将指望多个参数的函数转换为能够采用单个参数的函数,容许它们适合函数组合管道。以trace()
函数为例,从前面开始:
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
const trace = label => value => {
console.log(`${ label }: ${ value }`);
return value;
};
const g = n => n + 1;
const f = n => n * 2;
const h = pipe(
g,
trace('after g'),
f,
trace('after f'),
);
h(20);
/* after g: 21 after f: 42 */
复制代码
trace
定义了两个参数,可是一次只接受一个参数,容许咱们专门化内联函数。若是trace
不是curry
,咱们就不能以这种方式使用它。咱们必须像这样编写管道:
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
const trace = (label, value) => {
console.log(`${ label }: ${ value }`);
return value;
};
const g = n => n + 1;
const f = n => n * 2;
const h = pipe(
g,
// `trace`调用再也不是`point-free`风格,
// 引入中间变量, `x`.
x => trace('after g', x),
f,
x => trace('after f', x),
);
h(20);
复制代码
可是简单的curry
函数是不够的,还须要确保函数按正确的参数顺序来专门化它们。看看若是咱们再次curry trace()
会发生什么,可是翻转参数顺序:
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
const trace = value => label => {
console.log(`${ label }: ${ value }`);
return value;
};
const g = n => n + 1;
const f = n => n * 2;
const h = pipe(
g,
// the trace() calls can't be point-free,
// because arguments are expected in the wrong order.
x => trace(x)('after g'),
f,
x => trace(x)('after f'),
);
h(20);
复制代码
若是不想这样,可使用名为flip()
的函数解决该问题,该函数只是翻转两个参数的顺序:
const flip = fn => a => b => fn(b)(a);
复制代码
如今咱们能够建立一个flippedTrace
函数:
const flippedTrace = flip(trace);
复制代码
再这样使用这个flippedTrace
:
const flip = fn => a => b => fn(b)(a);
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
const trace = value => label => {
console.log(`${ label }: ${ value }`);
return value;
};
const flippedTrace = flip(trace);
const g = n => n + 1;
const f = n => n * 2;
const h = pipe(
g,
flippedTrace('after g'),
f,
flippedTrace('after f'),
);
h(20);
复制代码
能够发现这样也能够工做,可是 首先就应该以正确的方式去编写函数。这个样式有时称为“数据最后”,这意味着你应首先获取特殊参数,并获取该函数最后做用的数据。
看看这个函数的最初的形式:
const trace = label => value => {
console.log(`${ label }: ${ value }`);
return value;
};
复制代码
trace
的每一个应用程序都建立了一个在管道中使用的trace
函数的专用版本,其中label
被固定在返回的trace
部分应用程序中。因此这:
const trace = label => value => {
console.log(`${ label }: ${ value }`);
return value;
};
const traceAfterG = trace('after g');
复制代码
等同于下面这个:
const traceAfterG = value => {
const label = 'after g';
console.log(`${ label }: ${ value }`);
return value;
};
复制代码
若是咱们为traceAfterG
交换trace('after g')
,那就意味着一样的事情:
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
const trace = label => value => {
console.log(`${ label }: ${ value }`);
return value;
};
// The curried version of trace()
// saves us from writing all this code...
const traceAfterG = value => {
const label = 'after g';
console.log(`${ label }: ${ value }`);
return value;
};
const g = n => n + 1;
const f = n => n * 2;
const h = pipe(
g,
traceAfterG,
f,
trace('after f'),
);
h(20);
复制代码
curried
函数是一个函数,经过取第一个参数,一次一个地获取多个参数,并返回一系列函数,每一个函数接受下一个参数,直到全部参数都已修复,而且函数应用程序能够完成,此时返回结果值。
部分应用程序( partial application )是一个已经应用于某些 - 但还没有所有参数参与的函数。函数已经应用的参数称为固定参数。
point-free style
是一种定义函数而不引用其参数的方法。一般,经过调用返回函数的函数(例如curried
函数)来建立point-free
函数。
curried
函数很是适合函数组合 ,由于它们容许你轻松地将n元
函数转换为函数组合管道所需的一元函数形式:管道中的函数必须只接收一个参数。
数据最后 的功能便于功能组合,由于它们能够轻松地用于point-free style
。