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

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(下)

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏用户2442861的专栏

对vector等STL标准容器进行排序操作

STL几乎封装了所有的数据结构中的算法,从链表到队列,从向量到堆栈,对hash到二叉树,从搜索到排序,从增加到删除......可以说,如果你理解了STL,你会...

1182
来自专栏程序猿DD

第六章 正则表达式的构建

第六章 正则表达式的构建 对于一门语言的掌握程度怎么样,可以有两个角度来衡量:读和写。 不仅要看懂别人的解决方案,也要能独立地解决问题。代码是这样,正则表达式也...

1856
来自专栏冰霜之地

Google S2 中的四叉树求 LCA 最近公共祖先

首先需要回顾一下希尔伯特曲线的生成方式,具体代码见笔者上篇文章的分析,在这个分析中,有4个方向比较重要,接下来的分析需要,所以把这4个方向的图搬过来。

633
来自专栏小樱的经验随笔

基数排序与桶排序,计数排序【详解】

桶排序简单入门篇^-^ 在我们生活的这个世界中到处都是被排序过的东东。站队的时候会按照身高排序,考试的名次需要按照分数排序,网上购物的时候会按照价格排序,电子邮...

3137
来自专栏生信技能树

(11)仿写bowtie-生信菜鸟团博客2周年精选文章集

然后仿写了bowtie,对我的编程技术提高非常有帮助。目录如下: 自己动手写bowtie第一讲:BWT算法详解并建立索引 自己动手写bowtie第二讲:优化索引...

3046
来自专栏阿凯的Excel

巧妙解决二维表信息匹配问题

1192
来自专栏北京马哥教育

这段代码很Pythonic | 相见恨晚的 itertools 库

1503
来自专栏听雨堂

Python学习笔记(1):列表元组结构

Python的列表元组功能强大,令人印象深刻。一是非常灵活,二是便于集体操作。特别是以元组作为列表项的结构,和数据访问的结果能够对应起来,和习惯的二维表理解上也...

1966
来自专栏tkokof 的技术,小趣及杂念

位运算实用指南

说明 : 想来这应该是初次接触移位操作符时一定会了解到的知识点,根据2进制的整数表示方法应该不难理解,原因细节不再赘述~

531
来自专栏Lambda

JavaScript排序算法详解

JS家的排序算法 引子 有句话怎么说来着: 雷锋推倒雷峰塔,Java implements JavaScript. 当年,想凭借抱Java大腿火一把...

2217

扫码关注云+社区