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

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

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

JavaScript 轻量级函数式编程

附录 A:Transducing

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

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

说实话,即便我已经教过 transducing 不少次了,在写这一章的时候,我仍然须要花不少脑力去理清楚这个技术。因此,若是你看这一章看的很疑惑也不必感到沮丧。把这一章加个书签,等你以为你差很少能理解时再回头看看。github

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