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

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

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

JavaScript 轻量级函数式编程

附录 A:Transducing(下)

组合柯里化

这一步是最棘手的。因此请慢慢的用心的阅读。github

让咱们看看没有将 listCombination(..) 传递给柯里化函数的样子:编程

var x = curriedMapReducer( strUppercase );
var y = curriedFilterReducer( isLongEnough );
var z = curriedFilterReducer( isShortEnough );
复制代码

看看这三个中间函数 x(..)y(..)z(..)。每一个函数都指望获得一个单一的组合函数并产生一个 reducer 函数。小程序

记住,若是咱们想要全部这些的独立的 reducer,咱们能够这样作:微信小程序

var upperReducer = x( listCombination );
var longEnoughReducer = y( listCombination );
var shortEnoughReducer = z( listCombination );
复制代码

可是,若是你调用 y(z),会获得什么呢?当把 z 传递给 y(..) 调用,而不是 combinationFn(..) 时会发生什么呢?这个返回的 reducer 函数内部看起来像这样:数组

function reducer(list,val) {
	if (isLongEnough( val )) return z( list, val );
	return list;
}
复制代码

看到 z(..) 里面的调用了吗? 这看起来应该是错误的,由于 z(..) 函数应该只接收一个参数(combinationFn(..)),而不是两个参数(list 和 val)。这和要求不匹配。不行。微信

咱们来看看组合 y(z(listCombination))。咱们将把它分红两个不一样的步骤:app

var shortEnoughReducer = z( listCombination );
var longAndShortEnoughReducer = y( shortEnoughReducer );
复制代码

咱们建立 shortEnoughReducer(..),而后将它做为 combinationFn(..) 传递给 y(..),生成 longAndShortEnoughReducer(..)。多读几遍,直到理解。异步

如今想一想: shortEnoughReducer(..)longAndShortEnoughReducer(..) 的内部构造是什么样的呢?你能想获得吗?

// shortEnoughReducer, from z(..):
function reducer(list,val) {
	if (isShortEnough( val )) return listCombination( list, val );
	return list;
}

// longAndShortEnoughReducer, from y(..):
function reducer(list,val) {
	if (isLongEnough( val )) return shortEnoughReducer( list, val );
	return list;
}
复制代码

你看到 shortEnoughReducer(..) 替代了 longAndShortEnoughReducer(..) 里面 listCombination(..) 的位置了吗? 为何这样也能运行?

由于 reducer(..) 的“形状”和 listCombination(..) 的形状是同样的。 换句话说,reducer 能够用做另外一个 reducer 的组合函数; 它们就是这样组合起来的! listCombination(..) 函数做为第一个 reducer 的组合函数,这个 reducer 又能够做为组合函数给下一个 reducer,以此类推。

咱们用几个不一样的值来测试咱们的 longAndShortEnoughReducer(..)

longAndShortEnoughReducer( [], "nope" );
// []

longAndShortEnoughReducer( [], "hello" );
// ["hello"]

longAndShortEnoughReducer( [], "hello world" );
// []
复制代码

longAndShortEnoughReducer(..) 会过滤出不够长且不够短的值,它在同一步骤中执行这两个过滤。这是一个组合 reducer!

再花点时间消化下。

如今,把 x(..) (生成大写 reducer 的产生器)加入组合:

var longAndShortEnoughReducer = y( z( listCombination) );
var upperLongAndShortEnoughReducer = x( longAndShortEnoughReducer );
复制代码

正如 upperLongAndShortEnoughReducer(..) 名字所示,它同时执行全部三个步骤 - 一个映射和两个过滤器!它内部看起来是这样的:

// upperLongAndShortEnoughReducer:
function reducer(list,val) {
	return longAndShortEnoughReducer( list, strUppercase( val ) );
}
复制代码

一个字符串类型的 val 被传入,由 strUppercase(..) 转换成大写,而后传递给 longAndShortEnoughReducer(..)。该函数只有在 val 知足足够长且足够短的条件时才将它添加到数组中。不然数组保持不变。

我花了几个星期来思考分析这种杂耍似的操做。因此别着急,若是你须要在这好好研究下,从新阅读个几(十几个)次。慢慢来。

如今来验证一下:

upperLongAndShortEnoughReducer( [], "nope" );
// []

upperLongAndShortEnoughReducer( [], "hello" );
// ["HELLO"]

upperLongAndShortEnoughReducer( [], "hello world" );
// []
复制代码

这个 reducer 成功的组合了和 map 和两个 filter,太棒了!

让咱们回顾一下咱们到目前为止所作的事情:

var x = curriedMapReducer( strUppercase );
var y = curriedFilterReducer( isLongEnough );
var z = curriedFilterReducer( isShortEnough );

var upperLongAndShortEnoughReducer = x( y( z( listCombination ) ) );

words.reduce( upperLongAndShortEnoughReducer, [] );
// ["WRITTEN","SOMETHING"]
复制代码

这已经很酷了,可是咱们可让它更好。

x(y(z( .. ))) 是一个组合。咱们能够直接跳过中间的 x / y / z 变量名,直接这么表示该组合:

var composition = compose(
	curriedMapReducer( strUppercase ),
	curriedFilterReducer( isLongEnough ),
	curriedFilterReducer( isShortEnough )
);

var upperLongAndShortEnoughReducer = composition( listCombination );

words.reduce( upperLongAndShortEnoughReducer, [] );
// ["WRITTEN","SOMETHING"]
复制代码

咱们来考虑下该组合函数中“数据”的流动:

  1. listCombination(..) 做为组合函数传入,构造 isShortEnough(..) 过滤器的 reducer。

  2. 而后,所获得的 reducer 函数做为组合函数传入,继续构造 isShortEnough(..) 过滤器的 reducer。

  3. 最后,所获得的 reducer 函数做为组合函数传入,构造 strUppercase(..) 映射的 reducer。

在前面的片断中,composition(..) 是一个组合函数,指望组合函数来造成一个 reducer;而这个 composition(..) 有一个特殊的标签:transducer。给 transducer 提供组合函数产生组合的 reducer:

// TODO:检查 transducer 是产生 reducer 仍是它自己就是 reducer

var transducer = compose(
	curriedMapReducer( strUppercase ),
	curriedFilterReducer( isLongEnough ),
	curriedFilterReducer( isShortEnough )
);

words
.reduce( transducer( listCombination ), [] );
// ["WRITTEN","SOMETHING"]
复制代码

注意:咱们应该好好观察下前面两个片断中的 compose(..) 顺序,这地方有点难理解。回想一下,在咱们的原始示例中,咱们先 map(strUppercase) 而后 filter(isLongEnough) ,最后 filter(isShortEnough);这些操做实际上也确实按照这个顺序执行的。但在第 4 章中,咱们了解到,compose(..) 一般是以相反的顺序运行。那么为何咱们不须要反转这里的顺序来得到一样的指望结果呢?来自每一个 reducer 的 combinationFn(..) 的抽象反转了操做顺序。因此和直觉相反,当组合一个 tranducer 时,你只须要按照实际的顺序组合就好!

列表组合:纯与不纯

咱们再来看一下咱们的 listCombination(..) 组合函数的实现:

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

虽然这种方法是纯的,但它对性能有负面影响。首先,它建立临时数组来包裹 val。而后,concat(..) 方法建立一个全新的数组来链接这个临时数组。每一步都会建立和销毁的不少数组,这不只对 CPU 不利,也会形成 GC 内存的流失。

下面是性能更好可是不纯的版本:

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

单独的考虑下 listCombination(..) ,毫无疑问,这是不纯的,这一般是咱们想要避免的。可是,咱们应该考虑一个更大的背景。

listCombination(..) 不是咱们彻底有交互的函数。咱们不直接在程序中的任何地方使用它,而只是在 transducing 的过程当中使用它。

回到第 5 章,咱们定义纯函数来减小反作用的目标只是限制在应用的 API 层级。对于底层实现,只要没有违反对外部是纯函数,就能够在函数内为了性能而变得不纯。

listCombination(..) 更多的是转换的内部实现细节。实际上,它一般由 transducing 库提供!而不是你的程序中进行交互的顶层方法。

底线:我认为甚至使用 listCombination(..) 的性能最优可是不纯的版本也是彻底能够接受的。只要确保你用代码注释记录下它不纯便可!

可选的组合

到目前为止,这是咱们用转换所获得的:

words
.reduce( transducer( listCombination ), [] )
.reduce( strConcat, "" );
// 写点什么
复制代码

这已经很是棒了,可是咱们还藏着最后一个的技巧。坦白来讲,我认为这部分可以让你迄今为止付出的全部努力变得值得。

咱们能够用某种方式实现只用一个 reduce(..) 来“组合”这两个 reduce(..) 吗? 不幸的是,咱们并不能将 strConcat(..) 添加到 compose(..) 调用中; 它的“形状”不适用于那个组合。

可是让咱们来看下这两个功能:

function strConcat(str1,str2) { return str1 + str2; }

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

若是你用心观察,能够看出这两个功能是如何互换的。它们以不一样的数据类型运行,但在概念上它们也是同样的:将两个值组合成一个。

换句话说, strConcat(..) 是一个组合函数!

这意味着若是咱们的最终目标是得到字符串链接而不是数组,咱们就能够用它代替 listCombination(..)

words.reduce( transducer( strConcat ), "" );
// 写点什么
复制代码

Boom! 这就是 transducing。

最后

深吸一口气,确实有不少要消化。

放空咱们的大脑,让咱们把注意力转移到如何在咱们的程序中使用转换,而不是关心它的工做原理。

回想起咱们以前定义的辅助函数,为清楚起见,咱们从新命名一下:

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

var transduceFilter = curry( function filterReducer(predicateFn,combinationFn){
	return function reducer(list,v){
		if (predicateFn( v )) return combinationFn( list, v );
		return list;
	};
} );
复制代码

还记得咱们这样使用它们:

var transducer = compose(
	transduceMap( strUppercase ),
	transduceFilter( isLongEnough ),
	transduceFilter( isShortEnough )
);
复制代码

transducer(..) 仍然须要一个组合函数(如 listCombination(..)strConcat(..))来产生一个传递给 reduce(..) (连同初始值)的 transduce-reducer 函数。

可是为了更好的表达全部这些转换步骤,咱们来作一个 transduce(..) 工具来为咱们作这些步骤:

function transduce(transducer,combinationFn,initialValue,list) {
	var reducer = transducer( combinationFn );
	return list.reduce( reducer, initialValue );
}
复制代码

这是咱们的运行示例,梳理以下:

var transducer = compose(
	transduceMap( strUppercase ),
	transduceFilter( isLongEnough ),
	transduceFilter( isShortEnough )
);

transduce( transducer, listCombination, [], words );
// ["WRITTEN","SOMETHING"]

transduce( transducer, strConcat, "", words );
// 写点什么
复制代码

不错,嗯! 看到 listCombination(..)strConcat(..) 函数能够互换使用组合函数了吗?

Transducers.js

最后,咱们来讲明咱们运行的例子,使用sensors-js库(github.com/cognitect-l… ):

var transformer = transducers.comp(
	transducers.map( strUppercase ),
	transducers.filter( isLongEnough ),
	transducers.filter( isShortEnough )
);

transducers.transduce( transformer, listCombination, [], words );
// ["WRITTEN","SOMETHING"]

transducers.transduce( transformer, strConcat, "", words );
// WRITTENSOMETHING
复制代码

看起来几乎与上述相同。

注意: 上面的代码段使用 transformers.comp(..) ,由于这个库提供这个 API,但在这种状况下,咱们从第 4 章的 compose(..) 也将产生相同的结果。换句话说,组合自己不是 transducing 敏感的操做。

该片断中的组合函数被称为 transformer ,而不是 transducer。那是由于若是咱们直接调用 transformer(listCombination)(或 transformer(strConcat)),那么咱们不会像之前那样获得一个直观的 transduce-reducer 函数。

transducers.map(..)transducers.filter(..) 是特殊的辅助函数,能够将常规的断言函数或映射函数转换成适用于产生特殊变换对象的函数(里面包含了 reducer 函数);这个库使用这些变换对象进行转换。此转换对象抽象的额外功能超出了咱们将要探索的内容,请参阅该库的文档以获取更多信息。

因为 transformer(..) 产生一个变换对象,而不是一个典型的二元 transduce-reducer 函数,该库还提供 toFn(..) 来使变换对象适应本地数组的 reduce(..) 方法:

words.reduce(
	transducers.toFn( transformer, strConcat ),
	""
);
// WRITTENSOMETHING
复制代码

into(..) 是另外一个提供的辅助函数,它根据指定的空/初始值的类型自动选择默认的组合函数:

transducers.into( [], transformer, words );
// ["WRITTEN","SOMETHING"]

transducers.into( "", transformer, words );
// WRITTENSOMETHING
复制代码

当指定一个空数组 [] 时,内部的 transduce(..) 使用一个默认的函数实现,这个函数就像咱们的 listCombination(..)。可是当指定一个空字符串 “” 时,会使用像咱们的 strConcat(..) 这样的方法。这很酷!

如你所见,transducers-js 库使转换很是简单。咱们能够很是有效地利用这种技术的力量,而不至于陷入定义全部这些中间转换器生产工具的繁琐过程当中去。

总结

Transduce 就是经过减小来转换。更具体点,transduer 是可组合的 reducer。

咱们使用转换来组合相邻的map(..)filter(..)reduce(..) 操做。咱们首先将 map(..)filter(..) 表示为 reduce(..),而后抽象出经常使用的组合操做来建立一个容易组合的一致的 reducer 生成函数。

transducing 主要提升性能,若是在延迟序列(异步 observables)中使用,则这一点尤其明显。

可是更普遍地说,transducing 是咱们针对那些不能被直接组合的函数,使用的一种更具声明式风格的方法。不然这些函数将不能直接组合。若是使用这个技术能像使用本书中的全部其余技术同样用的恰到好处,代码就会显得更清晰,更易读! 使用 transducer 进行单次 reduce(..) 调用比追踪多个 reduce(..) 调用更容易理解。

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

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

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


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