翻译连载 | 附录 A:Transducing(下)-《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇
关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTML 最坚实的梁柱;分享,是 CSS 里最闪耀的一瞥;总结,是 JavaScript 中最严谨的逻辑。经过捶打磨练,成就了本书的中文版。本书包含了函数式编程之精髓,希望可以帮助大家在学习函数式编程的道路上走的更顺畅。比心。
译者团队(排名不分先后):阿希、blueken、brucecham、cfanlife、dail、kyoko-df、l3ve、lilins、LittlePineapple、MatildaJin、冬青、pobusama、Cherry、萝卜、vavd317、vivaxy、萌萌、zhouyao
JavaScript 轻量级函数式编程
附录 A:Transducing(下)
组合柯里化
这一步是最棘手的。所以请慢慢的用心的阅读。
让我们看看没有将 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))
。我们将把它分成两个不同的步骤:
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"]
我们来考虑下该组合函数中“数据”的流动:
-
listCombination(..)
作为组合函数传入,构造isShortEnough(..)
过滤器的 reducer。 -
然后,所得到的 reducer 函数作为组合函数传入,继续构造
isShortEnough(..)
过滤器的 reducer。 -
最后,所得到的 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库(https://github.com/cognitect-labs/transducers-js ):
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官网:https://www.ikcamp.com
访问官网更快阅读全部免费分享课程:
《iKcamp出品|全网最新|微信小程序|基于最新版1.0开发者工具之初中级培训教程分享》
《iKcamp出品|基于Koa2搭建Node.js实战项目教程》
包含:文章、视频、源代码