函数式编程正在占领JavaScript世界。就在几年前,很少有JavaScript程序员甚至不知道什么是函数式编程,但是我在过去三年中看到的每个大型应用程序代码库都大量使用了函数式编程思想。

函数组成是将两个或多个函数组合在一起以产生新函数的过程。将函数组合在一起就像将一系列管道对齐在一起以使我们的数据流过。
简单地说,函数组合F
和g
可以定义为F(G(X))
,其评估由内而外-从右到左。换句话说,评估顺序为:
- x
- g
- f
让我们在代码中更仔细地研究一下。假设您要将用户的全名转换为URL标记,以便为每个用户提供一个个人资料页面。为此,您需要完成以下步骤:
- 将名称分成空格数组
- 将名称映射为小写
- 连字符
- 编码URI组件
这是一个简单的实现:
const toSlug = input => encodeURIComponent(
input.split(' ')
.map(str => str.toLowerCase())
.join('-')
);
不错…但是如果我告诉你它可能更具可读性呢?
想象这些操作中的每一个都有对应的可组合函数。可以这样写:
const toSlug = input => encodeURIComponent(
join('-')(
map(toLowerCase)(
split(' ')(
input
)
)
)
);
console.log(toSlug('JS Cheerleader')); // 'js-cheerleader'
这看起来比我们的第一次尝试更难读.
为了做到这一点,我们使用了常见的实用程序的可组合形式,例如split()
,join()
和map()
。这里是实现:
const curry = fn => (...args) => fn.bind(null, ...args);
const map = curry((fn, arr) => arr.map(fn));
const join = curry((str, arr) => arr.join(str));
const toLowerCase = str => str.toLowerCase();
const split = curry((splitOn, str) => str.split(splitOn));
除了“ toLowerCase()”外,所有这些函数的经过生产测试的版本都可以从Lodash / fp获得。您可以这样导入它们:
import { curry, map, join, split } from 'lodash/fp';
或像这样:
const curry = require('lodash/fp/curry');
const map = require('lodash/fp/map');
//...
我在这里有点懒。请注意,该柯里化从技术上讲并不是真正的柯里化,它总是会产生一元函数。相反,它是一个简单的部分应用程序。
回到我们的toSlug()
实现中,确实有一些让我感到困扰的事情:
const toSlug = input => encodeURIComponent(
join('-')(
map(toLowerCase)(
split(' ')(
input
)
)
)
);
console.log(toSlug('JS Cheerleader')); // 'js-cheerleader'
对我来说,这看起来像是很多嵌套,而且阅读起来有些混乱。我们可以使用可以自动为我们组合这些函数的函数来展平嵌套,这意味着它将从一个函数中获取输出,并自动将其修补到下一个函数的输入中,直到输出最终值为止。
想一下,我们有一个数组Extras实用程序,听起来像它做了类似的事情。它采用值列表,并对每个值应用一个函数,从而累积单个结果。值本身可以是函数。这个函数叫做reduce()
,但是为了匹配上面的组合行为,我们需要它减小从右到左而不是从左到右的位置。
好东西有一个reduceRight()
正是我们想要的东西:
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
像.reduce()
一样,数组.reduceRight()
方法使用一个reducer函数和一个初始值(x
)。我们遍历数组函数(从右到左),依次将每个函数应用于累加值(“ v”)。
使用compose,我们可以在没有嵌套的情况下重写上面的合成:
const toSlug = compose(
encodeURIComponent,
join('-'),
map(toLowerCase),
split(' ')
);
console.log(toSlug('JS Cheerleader')); // 'js-cheerleader'
当然,compose()
也随lodash / fp一起提供:
import { compose } from 'lodash/fp';
或者
const compose = require('lodash/fp/compose');
当您从内到外从构图的数学形式来思考时,函数合成很棒。但是,如果您想从左到右的顺序去思考怎么办?
还有另一种形式,通常称为pipe()
。Lodash将其称为flow()
:
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
const fn1 = s => s.toLowerCase();
const fn2 = s => s.split('').reverse().join('');
const fn3 = s => s + '!'
const newFunc = pipe(fn1, fn2, fn3);
const result = newFunc('Time'); // emit!
注意,实现完全一样 compose()
,除了我们使用.reduce()
代替.reduceRight()
,这减少从左到右,而不是从右到左。
让我们看一下通过pipe()
实现的toSlug()
函数:
const toSlug = pipe(
split(' '),
map(toLowerCase),
join('-'),
encodeURIComponent
);
console.log(toSlug('JS Cheerleader')); // 'js-cheerleader'
对我来说,这更容易阅读。
核心函数程序员根据函数组成定义整个应用程序。我经常使用它来消除对临时变量的需要。仔细看一下toSlug()的pipe()版本,您可能会注意到一些特别的地方。
在命令式编程中,当您对某个变量执行转换时,您会在转换的每个步骤中找到对该变量的引用。上面的pipe()
实现是以无点形式编写的,这意味着它根本无法识别要对其进行操作的参数。
我经常在单元测试和Redux状态缩减器之类的过程中使用管道,以消除对中间变量的需要,这些中间变量仅用于保存一个操作与下一个操作之间的瞬态值。
一开始听起来可能很奇怪,但是当您进行实践练习时,会发现在函数式编程中,您正在使用非常抽象的,通用的函数,其中事物的名称无关紧要。名称只是妨碍。您可能开始认为变量是不必要的样板。
就是说,我认为无分的风格可以采取得太过分。它可能变得过于密集,难以理解,但是如果您感到困惑,这里有个小技巧……您可以利用流程来跟踪正在发生的事情:
const trace = curry((label, x) => {
console.log(`== ${ label }: ${ x }`);
return x;
});
使用方法如下:
const toSlug = pipe(
trace('input'),
split(' '),
map(toLowerCase),
trace('after map'),
join('-'),
encodeURIComponent
);
console.log(toSlug('JS Cheerleader'));
// '== input: JS Cheerleader'
// '== after map: js,cheerleader'
// 'js-cheerleader'
“ trace()”只是更通用的“ tap()”的一种特殊形式,它使您可以对流经管道的每个值执行一些操作。得到它?管道?轻敲?你可以这样写tap()
:
const tap = curry((fn, x) => {
fn(x);
return x;
});
现在您可以看到trace()
只是一个特殊情况的tap()
:
const trace = label => {
return tap(x => console.log(`== ${ label }: ${ x }`));
};
您应该开始了解函数式编程是什么,以及部分应用程序和currying如何与函数组合协作,以帮助您以更少的样板编写更具可读性的程序。
参考
Master the JavaScript Interview: What is Function Composition?
网友评论