【译】函数式编程-柯里化和函数合成

原文:medium.com/javascript-…
做者:Eric Elliott
翻译:前端小白javascript

注意:这篇文章是 "Composing Software" 学习函数式编程 这个系列(如今已出版为一本书)里面的一部分。前端

什么是函数柯里化

一个被柯里化的函数是一个本来接受多个参数,转变成接受单一参数的函数。给定一个有3个参数的函数,将他柯里化后,它将接受一个参数并返回一个函数,这个返回的函数又接受下一个参数,接下来又返回一个函数接受第三个参数,最后一个函数返回接受全部参数后函数运行的结果。java

你可使用数量不定的参数来实现柯里化,好比给定一个函数接受两个参数 ab,将函数柯里化后返回两个参数的和shell

// add = a => b => Number
const add = a => b => a + b;
复制代码

咱们须要调用这两个函数来使用 add 函数,调用函数的语法就是在函数引用后面加上 (),当一个函数返回另外一个函数时,能够经过添加一组额外的括号当即调用返回的函数:编程

const result = add(2)(3); // => 5
复制代码

首先,这个函数接受 a 参数,而后返回一个新函数,新函数接受 b 参数,而后返回 ab的和,每次都只接受一个参数。若是函数有更多的参数,那就简单地继续返回新函数,直到全部的参数都挨个被接收,而后程序运行完成。数组

add 函数接受一个参数,而后返回了它自身的一部分,这部分中有个变量 afixed 在该函数的闭包中,闭包就是某个函数有个和它捆绑在一块儿的一个词法做用域,闭包是在函数建立期间的运行时创建的,fixed 意味着闭包里面的变量只会在闭包环境中被赋值。bash

上面例子中的括号表示函数被调用:add 函数被调用,同时传入了参数 2,返回一个部分应用的函数,这时 a 的值被固定为 2。这时候返回值不会被赋值给某个变量或者以其余方式去使用,咱们立刻又会将 3 传入,并调用这个返回的函数,如今程序结束,返回结果 5闭包

什么是部分应用(偏函数)

部分应用是一个应用了一个或者多个参数的函数,没有接受所有参数。换句话说,部分应用就是一个函数,这个函数有一些参数被固化在它的闭包做用域范围内。一个函数的某些参数是固定的,咱们称之为部分应用app

区别

部分应用能够根据须要一次使用任意多或任意少的参数,而柯里化的函数老是返回一个一元函数,即只接受一个参数的函数。函数式编程

全部的柯里化函数都返回部分应用,但不是全部的部分应用都是柯里化函数的结果。

柯里化函数的一元需求是一个很重要的特性。

什么是 point-free style

point-free style 是一种编程风格,函数在定义时,不要对函数的参数进行引用,咱们看看下面的函数定义:

function foo (/* 这里声明参数*/) {
  // ...
}
const foo = (/* 这里声明参数 */) => // ...
const foo = function (/* 这里声明参数 */) {
  // ...
}
复制代码

如何在不引用所需参数的状况下用JavaScript定义函数?咱们不能使用 function 关键字,也不能使用箭头函数(=>),由于这些都要求正式的参数声明(会引用该参数)。因此咱们要作的是调用一个会返回函数的函数。

使用 point-free style 建立一个函数,该函数会将你传入的任何数字加1,记住,咱们已经有了一个名为 add 的函数,它接受一个数字并返回一个部分应用的函数,其第一个参数被固化为传入的任何值。咱们可使用它来建立一个 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 替代,整个程序结束,返回 13 的和。

全部柯里化函数都是高阶函数的一种形式,它容许为手头的特定用例建立一个原始函数的特定版本。

为何须要柯里化

函数柯里化在函数组合上下文中特别有用

在代数中,给定两个函数 gf

g: a -> b
f: b -> c
复制代码

你能够将他们组合成一个新函数,从 a 直接到 ch 函数

// 代数定义,借助 Haskell 中的 `.` 组合运算符
h: a -> c
h = f . g = f(g(x))
复制代码

在Javascript中:

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) => x => f(g(x));
复制代码

可是这一次只能组合两个函数。在代数中,能够这样写:

f . g . h
复制代码

咱们能够编写一个函数来组合任意多个函数,换句话说,compose() 建立一个函数管道,其中一个函数的输出会链接到下一个函数的输入。

我一般喜欢这么写:

const compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x);
复制代码

这种写法接受任意数量的函数并返回一个接受初始值的函数,而后使用 reduceRight()fns 中的 f 从右到左的迭代,并调用,获得最后累积的值,在这个函数里面,咱们用累加器积累的 ycompose() 这个函数返回的函数的返回值。

如今咱们能够这样组合:

const g = n => n + 1;
const f = n => n * 2;
// 将 `x => f(g(x))` 用 `compose(f, g)` 替换
const h = compose(f, g);
h(20); //=> 42
复制代码

追踪

函数组合使用了 point-free style 使代码很是简洁、可读。可是也为代码调试带来了不便,若是你想监测两个函数之间的值,该怎么作?trace() 是一个十分方便的工具,可让你作到这一点。 它采用了函数柯里化的形式:

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;
/* 注意函数调用顺序是从下至上 */
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);
复制代码

如今上面的代码能够这样写:

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 */
复制代码

柯里化和函数组合结合

即便没有与函数组合结合起来,柯里化也是一个实用的抽象功能,咱们可使用它来专门化一个函数。例如,柯里化版本的 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]
复制代码

可是柯里化真正的优点是它简化了函数组合,一个函数能够接受任意数量的输入,但只有一个输出,为了使函数可组合,输出类型必须与预期的输入类型一致:

f: a => b
g:      b => c
h: a    =>   c
复制代码

若是上面的 g 函数但愿接受两个参数,f 函数的输出就与 g 函数的输入不一致了:

f: a => b
g:     (x, b) => c
h: a    =>   c
复制代码

在这种状况下,咱们怎样能够将 x 传给 g,答案就是将 g 柯里化

记住,柯里化函数的定义就是一个函数,它一次接受多个参数,取第一个参数并返回一系列函数,每一个函数取下一个参数,直到收集到全部参数。

定义中的关键字词是 一次接受一个,柯里化函数对于函数组合来讲很是方便,缘由就是它们将指望接受多个参数的函数转换为能够接受单个参数的函数,使他们能够应用在函数组合管道中

之前面的 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() 函数,若是它没哟被柯里化,咱们就不能这样使用,咱们的整个函数组合管道看起来像这样

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,
  // the trace() calls are no longer point-free,
  // introducing the intermediary variable, `x`.
  x => trace('after g', x),
  f,
  x => trace('after f', x),
);
h(20);
复制代码

可是简单的柯里化一个函数是不够的。还须要将这个函数专门化并确保函数指望的参数的顺序,若是咱们再次将 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);
复制代码

像这样使用

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);
复制代码

但更好的方式是先正确编写函数,这种风格有人称为 data last,意思就是应该将专门化的参数放在前面,而后将函数使用到的数据放在最后,以函数的原始形式

const trace = label => value => {
  console.log(`${ label }: ${ value }`);
  return value;
};
复制代码

每一个 trace() 对应一个 label,这是一个用于函数管道里面的专门化的 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;
};
复制代码

若是咱们将 trace('after g')traceAfterG 替换,意思是同样的:

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);
复制代码

结论

柯里化函数是一个本来接受多个参数,转变成接受单一参数的函数,经过第一个参数,并返回一系列函数,接着又取下一个参数,直到全部的参数被用尽,程序结束,返回结果。

部分应用(偏函数是指一个函数,只接受了部分,不是所有参数,那些已经被接受了的参数称为固定参数

Point-free style 是一种编程风格,函数在定义时,不要对函数的参数进行引用,一般咱们经过调用一个返回值为函数的函数来建立一个 point-free 函数,好比柯里化函数。

柯里化函数对于函数组合很是有用,由于它容许你轻松地将一个n元函数转换为函数组合管道所需的一元函数形式:函数管道中的函数必须只接受一个参数。

Data last 函数对于函数组合来讲很方便,他们能够很容易的使用 point-free style 形式

相关文章
相关标签/搜索