阅读 827

翻译连载 | 附录 A:Transducing(上)-《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇

关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTML 最坚实的梁柱;分享,是 CSS 里最闪耀的一瞥;总结,是 JavaScript 中最严谨的逻辑。经过捶打磨练,成就了本书的中文版。本书包含了函数式编程之精髓,希望可以帮助大家在学习函数式编程的道路上走的更顺畅。比心。

译者团队(排名不分先后):阿希bluekenbrucechamcfanlifedailkyoko-dfl3velilinsLittlePineappleMatildaJin冬青pobusamaCherry萝卜vavd317vivaxy萌萌zhouyao

JavaScript 轻量级函数式编程

附录 A:Transducing

Transducing 是我们这本书要讲到的更为高级的技术。它继承了第 8 章数组操作的许多思想。

我不会把 Transducing 严格的称为“轻量级函数式编程”,它更像是一个顶级的技巧。我把这个技术留到附录来讲意味着你现在很可能并不需要关心它,当你确保你已经非常熟悉整本书的主要内容,你可以再回头看看这一章节。

说实话,即使我已经教过 transducing 很多次了,在写这一章的时候,我仍然需要花很多脑力去理清楚这个技术。所以,如果你看这一章看的很疑惑也没必要感到沮丧。把这一章加个书签,等你觉得你差不多能理解时再回头看看。

Transducing 就是通过减少来转换。

我知道这听起来很令人费解。但是让我们来看看它有多强大。实际上,我认为这是你掌握了轻量级函数式编程后可以做的最好的例证之一。

和这本书的其他部分一样,我的方法是先解释为什么使用这个技术,然后如何使用,最后归结为简单的这个技术到底是什么样的。这通常会有多学很多东西,但是我觉得用这种方式你会更深入的理解它。

首先,为什么

让我们从扩展我们在第 3 章中介绍的例子开始,测试单词是否足够短和/或足够长:

function isLongEnough(str) {
	return str.length >= 5;
}

function isShortEnough(str) {
	return str.length <= 10;
}
复制代码

在第 3 章中,我们使用这些断言函数来测试一个单词。然后在第 8 章中,我们学习了如何使用像 filter(..) 这样的数组操作来重复这些测试。例如:

var words = [ "You", "have", "written", "something", "very", "interesting" ];

words
.filter( isLongEnough )
.filter( isShortEnough );
// ["written","something"]
复制代码

这个例子可能并不明显,但是这种分开操作相同数组的方式具有一些不理想的地方。当我们处理一个值比较少的数组时一切都还好。但是如果数组中有很多值,每个 filter(..) 分别处理数组的每个值会比我们预期的慢一点。

当我们的数组是异步/懒惰(也称为 observables)的,随着时间的推移响应事件处理(见第 10 章),会出现类似的性能问题。在这种情况下,一次事件只有一个值,因此使用两个单独的 filter(..) 函数处理这些值并不是什么大不了的事情。

但是,不太明显的是每个 filter(..) 方法都会产生一个单独的 observable 值。从一个 observable 值中抽出一个值的开销真的可以加起来(译者注:详情请看第 10 章的“积极的 vs 惰性的”这一节)。这是真实存在的,因为在这些情况下,处理数千或数百万的值并不罕见; 所以,即使是这么小的成本也会很快累加起来。

另一个缺点是可读性,特别是当我们需要对多个数组(或 observable)重复相同的操作时。例如:

zip(
	list1.filter( isLongEnough ).filter( isShortEnough ),
	list2.filter( isLongEnough ).filter( isShortEnough ),
	list3.filter( isLongEnough ).filter( isShortEnough )
)
复制代码

显得很重复,对不对?

如果我们可以将 isLongEnough(..) 断言与 isShortEnough(..) 断言组合在一起是不是会更好一点呢(可读性和性能)?你可以手动执行:

function isCorrectLength(str) {
	return isLongEnough( str ) && isShortEnough( str );
}
复制代码

但这不是函数式编程的方式!

在第 8 章中,我们讨论了融合 —— 组合相邻映射函数。回忆一下:

words
.map(
	pipe( removeInvalidChars, upper, elide )
);
复制代码

不幸的是,组合相邻断言函数并不像组合相邻映射函数那样容易。为什么呢?想想断言函数长什么“样子” —— 一种描述输入和输出的学术方式。它接收一个单一的参数,返回一个 true 或 false。

如果你试着用 isshortenough(islongenough(str)),这是行不通的。因为 islongenough(..) 会返回 true 或者 false ,而不是返回 isshortenough(..) 所要的字符串类型的值。这可真倒霉。

试图组合两个相邻的 reducer 函数同样是行不通的。reducer 函数接收两个值作为输入,并返回单个组合值。reducer 函数的单一返回值也不能作为参数传到另一个需要两个输入的 reducer 函数中。

此外,reduce(..) 辅助函数可以接收一个可选的 initialValue 输入。有时可以省略,但有时候它又必须被传入。这就让组合更复杂了,因为一个 reduce(..) 可能需要一个 initialValue,而另一个 reduce(..) 可能需要另一个 initialValue。所以我们怎么可能只用某种组合的 reducer 来实现 reduce(..) 呢。

考虑像这样的链:

words
.map( strUppercase )
.filter( isLongEnough )
.filter( isShortEnough )
.reduce( strConcat, "" );
// "WRITTENSOMETHING"
复制代码

你能想出一个组合能够包含 map(strUppercase)filter(isLongEnough)filter(isShortEnough)reduce(strConcat) 所有这些操作吗?每种操作的行为是不同的,所以不能直接组合在一起。我们需要把它们修改下让它们组合在一起。

希望这些例子说明了为什么简单的组合不能胜任这项任务。我们需要一个更强大的技术,而 transducing 就是这个技术。

如何,下一步

让我们谈谈我们该如何得到一个能组合映射,断言和/或 reducers 的框架。

别太紧张:你不必经历编程过程中所有的探索步骤。一旦你理解了 transducing 能解决的问题,你就可以直接使用函数式编程库中的 transduce(..) 工具继续你应用程序的剩余部分!

让我们开始探索吧。

把 Map/Filter 表示为 Reduce

我们要做的第一件事情就是将我们的 filter(..)map(..)调用变为 reduce(..) 调用。回想一下我们在第 8 章是怎么做的:

function strUppercase(str) { return str.toUpperCase(); }
function strConcat(str1,str2) { return str1 + str2; }

function strUppercaseReducer(list,str) {
	list.push( strUppercase( str ) );
	return list;
}

function isLongEnoughReducer(list,str) {
	if (isLongEnough( str )) list.push( str );
	return list;
}

function isShortEnoughReducer(list,str) {
	if (isShortEnough( str )) list.push( str );
	return list;
}

words
.reduce( strUppercaseReducer, [] )
.reduce( isLongEnoughReducer, [] )
.reduce( isShortEnough, [] )
.reduce( strConcat, "" );
// "WRITTENSOMETHING"
复制代码

这是一个不错的改进。我们现在有四个相邻的 reduce(..) 调用,而不是三种不同方法的混合。然而,我们仍然不能 compose(..) 这四个 reducer,因为它们接受两个参数而不是一个参数。

在 8 章,我们偷了点懒使用了数组的 push 方法而不是 concat(..) 方法返回一个新数组,导致有副作用。现在让我们更正式一点:

function strUppercaseReducer(list,str) {
	return list.concat( [strUppercase( str )] );
}

function isLongEnoughReducer(list,str) {
	if (isLongEnough( str )) return list.concat( [str] );
	return list;
}

function isShortEnoughReducer(list,str) {
	if (isShortEnough( str )) return list.concat( [str] );
	return list;
}
复制代码

在后面我们会来头看看这里是否需要 concat(..)

参数化 Reducers

除了使用不同的断言函数之外,两个 filter reducers 几乎相同。让我们把这些 reducers 参数化得到一个可以定义任何 filter-reducer 的工具函数:

function filterReducer(predicateFn) {
	return function reducer(list,val){
		if (predicateFn( val )) return list.concat( [val] );
		return list;
	};
}

var isLongEnoughReducer = filterReducer( isLongEnough );
var isShortEnoughReducer = filterReducer( isShortEnough );
复制代码

同样的,我们把 mapperFn(..) 也参数化来生成 map-reducer 函数:

function mapReducer(mapperFn) {
	return function reducer(list,val){
		return list.concat( [mapperFn( val )] );
	};
}

var strToUppercaseReducer = mapReducer( strUppercase );
复制代码

我们的调用链看起来是一样的:

words
.reduce( strUppercaseReducer, [] )
.reduce( isLongEnoughReducer, [] )
.reduce( isShortEnough, [] )
.reduce( strConcat, "" );
复制代码

提取共用组合逻辑

仔细观察上面的 mapReducer(..)filterReducer(..) 函数。你发现共享功能了吗?

这部分:

return list.concat( .. );

// 或者
return list;
复制代码

让我们为这个通用逻辑定义一个辅助函数。但是我们叫它什么呢?

function WHATSITCALLED(list,val) {
	return list.concat( [val] );
}
复制代码

WHATSITCALLED(..) 函数做了些什么呢,它接收两个参数(一个数组和另一个值),将值 concat 到数组的末尾返回一个新的数组。所以这个 WHATSITCALLED(..) 名字不合适,我们可以叫它 listCombination(..)

function listCombination(list,val) {
	return list.concat( [val] );
}
复制代码

我们现在用 listCombination(..) 来重新定义我们的 reducer 辅助函数:

function mapReducer(mapperFn) {
	return function reducer(list,val){
		return listCombination( list, mapperFn( val ) );
	};
}

function filterReducer(predicateFn) {
	return function reducer(list,val){
		if (predicateFn( val )) return listCombination( list, val );
		return list;
	};
}
复制代码

我们的调用链看起来还是一样的(这里就不重复写了)。

参数化组合

我们的 listCombination(..) 小工具只是组合两个值的一种方式。让我们将它的用途参数化,以使我们的 reducers 更加通用:

function mapReducer(mapperFn,combinationFn) {
	return function reducer(list,val){
		return combinationFn( list, mapperFn( val ) );
	};
}

function filterReducer(predicateFn,combinationFn) {
	return function reducer(list,val){
		if (predicateFn( val )) return combinationFn( list, val );
		return list;
	};
}
复制代码

使用这种形式的辅助函数:

var strToUppercaseReducer = mapReducer( strUppercase, listCombination );
var isLongEnoughReducer = filterReducer( isLongEnough, listCombination );
var isShortEnoughReducer = filterReducer( isShortEnough, listCombination );
复制代码

将这些实用函数定义为接收两个参数而不是一个参数不太方便组合,因此我们使用我们的 curry(..) (柯里化)方法:

var curriedMapReducer = curry( function mapReducer(mapperFn,combinationFn){
	return function reducer(list,val){
		return combinationFn( list, mapperFn( val ) );
	};
} );

var curriedFilterReducer = curry( function filterReducer(predicateFn,combinationFn){
	return function reducer(list,val){
		if (predicateFn( val )) return combinationFn( list, val );
		return list;
	};
} );

var strToUppercaseReducer =
	curriedMapReducer( strUppercase )( listCombination );
var isLongEnoughReducer =
	curriedFilterReducer( isLongEnough )( listCombination );
var isShortEnoughReducer =
	curriedFilterReducer( isShortEnough )( listCombination );
复制代码

这看起来有点冗长而且可能不是很有用。

但这实际上是我们进行下一步推导的必要条件。请记住,我们的最终目标是能够 compose(..) 这些 reducers。我们快要完成了。

 附录 A:Transducing(下)---- 四天后更新

** 【上一章】翻译连载 | 第 11 章:融会贯通 -《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇 **

iKcamp原创新书《移动Web前端高效开发实战》已在亚马逊、京东、当当开售。

iKcamp官网:www.ikcamp.com 访问官网更快阅读全部免费分享课程: 《iKcamp出品|全网最新|微信小程序|基于最新版1.0开发者工具之初中级培训教程分享》 《iKcamp出品|基于Koa2搭建Node.js实战项目教程》 包含:文章、视频、源代码


2019年,iKcamp原创新书《Koa与Node.js开发实战》已在京东、天猫、亚马逊、当当开售啦!

关注下面的标签,发现更多相似文章
评论