js的curry和函数组合

栏目: JavaScript · 发布时间: 5年前

内容简介:原文:译文:提醒:本文略长,慎重阅读

原文: Eric Elliott - Curry and Function Composition

译文: curry和函数组合

提醒:本文略长,慎重阅读

之前看到有文章说柯里化函数,大致看了下,就是高阶函数,只是名字听起来比较高大上一点,今天逛 medium 又发现了这个,看了下感觉还不错,有涉及到闭包,涉及到 point-free style , 并不是一股脑的安利 demo 了事,这个得记录下。

什么是 curried 函数

curried 函数是个一次一个的去获取多个参数的函数。 再明白点,就是比如 给定一个带有3个参数的函数, curried 版的函数将接受一个参数并返回一个接受下一个参数的函数,该函数返回一个接受第三个参数的函数。最后一个函数返回将函数应用于其所有参数的结果。

看下面的例子,例如,给定两个数字, abcurried 形式,返回 ab 的总和:

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

然后就可以直接调用了:

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

首先,函数接受 a 作为参数,然后返回一个新的函数,然后将 b 传递给这个新的函数,最后返回 ab 的和。每次只传递一个参数。如果函数有更多参数,不止上面的 a,b 两个参数,它可以继续像上面这样返回新的函数,直到最后结束这个函数。

add 函数接受一个参数之后,然后在这个闭包的范围内返回固定的一部分功能。闭包就是与词法范围捆绑在一起的函数。在运行函数创建时创建了闭包,可以在 这里了解更多固定 的意思是说变量在闭包的绑定范围内赋值。

在来看看上面的代码: add 用参数 2 去调用,返回一个部分应用的函数,并且固定 a2 。我们不是将返回值赋值给变量或以其他方式使用它,而是通过在括号中将 3 传递给它来立即调用返回的函数,从而完成整个函数并返回 5

什么是部分功能

部分应用程序( 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 ,并且应用程序完成,返回 13 之和。

所有 curried 函数都是高阶函数的一种形式,它允许你为手头的特定用例创建原始函数的专用版本。

为什么要 curry

curried 函数在函数组合的上下文中特别有用。

在代数中,给出了两个函数 fg

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

我们可以将这些函数组合在一起创建一个新的函数( h ), ha 直接到 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和功能组合一起

即使在函数组合的上下文之外, 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


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

现代操作系统(第3版)

现代操作系统(第3版)

Andrew S. Tanenbaum / 陈向群、马洪兵 / 机械工业出版社 / 2009-7 / 75.00元

本书是操作系统领域的经典之作,与第2版相比,增加了关于Linux、Windows Vista和Symbian操作系统的详细介绍。书中集中讨论了操作系统的基本原理,包括进程、线程、存储管理、文件系统、输入/输出、死锁等,同时还包含了有关计算机安全、多媒体操作系统、掌上计算机操作系统、微内核、多核处理机上的虚拟机以及操作系统设计等方面的内容。此外,还在第2版的基础上对部分习题进行了增删,更有助于读者学......一起来看看 《现代操作系统(第3版)》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

随机密码生成器
随机密码生成器

多种字符组合密码

MD5 加密
MD5 加密

MD5 加密工具