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

时间: 2024-10-10 15:38:58

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

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

原文地址:Functional-Light-JS 原文作者:Kyle Simpson-<You-Dont-Know-JS>作者 关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTML 最坚实的梁柱:分享,是 CSS 里最闪耀的一瞥:总结,是 JavaScript 中最严谨的逻辑.经过捶打磨练,成就了本书的中文版.本书包含了函数式编程之精髓,希望可以帮助大家在学习函数式编程的道路上走的更顺畅.比心. 译者团队(排名不分先后):阿希.blueken.brucecham.cfanlife.d

全本 | iKcamp翻译 | 《JavaScript 轻量级函数式编程》|《你不知道的JS》姊妹篇

原文地址:Functional-Light-JS 原文作者:Kyle Simpson - <You-Dont-Know-JS>作者 译者团队(排名不分先后):阿希.blueken.brucecham.cfanlife.dail.kyoko-df.l3ve.lilins.LittlePineapple.MatildaJin.冬青.pobusama.Cherry.萝卜.vavd317.vivaxy.萌萌.zhouyao 关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTML 最坚实的梁柱:

JavaScript中函数式编程的体现--map和reduce

最近在学JavaScript,中间看到map和reduce方法,觉得挺有意思的,边学边写下这篇博客. 这两个函数都在某种程度上体现了函数式编程的思想,即将函数作为传入另一个函数的参数. map()方法的调用者一般是个数组,参数是一个函数,称为callback,返回值是一个由原数组中每个元素执行给定callback函数的返回值组成的新数组. 也就是说,当你用map()方法时,是将组成数组中的每个元素作为参数,传进给定的函数,如果这个函数是有返回值的,则将每次执行函数得到的返回值组成一个新的数组返回

JavaScript中函数式编程中文翻译

原著由 Dan Mantyla 编写 近几年来,随着 Haskell.Scala.Clojure 等学院派原生支持函数式编程的偏门语言越来越受到关注,同时主流的 Java.JavaScript.Python 甚至 C++都陆续支持函数式编程.特别值得一提的是,在 nodejs 出现后,JavaScript 成为第一种从前端到后台的全栈语言,而且 JavaScript 支持多范式编程.应用函数式编程的最大挑战就是思维模式的改变———从传统面向对象的范式变为函数式编程范式. <JavaScript

Scala:用于Java的轻量级函数式编程

Scala为Java开发提供了轻量级的代码选项,但是学习过程可能会很艰难.了解有关Scala的知识以及是否值得采用. 基于Java的语言通常涉及冗长的语法和特定于领域的语言,用于测试,解析和数值计算过程.这些事情可能是开发人员的祸根,因为成堆的重复代码需要开发人员花费更多的时间进行梳理才能发现错误. 作为一种通用的编程语言,Scala可以通过结合面向对象的样式和功能样式来帮助减轻这些问题.为了减轻语法复杂性,Scala还将命令性编程与功能性编程相融合,并且可以方便地使用其对庞大的Java库生态系

JavaScript 与函数式编程

原文:https://bethallchurch.github.io/JavaScript-and-Functional-Programming/ 译文:http://www.zcfy.cc/article/1013 译者注:推荐一篇译文,<函数式编程术语解析>. 本文是我在 2016 年 7 月 29 号听 Kyle Simpson 精彩的课程<Functional-Light JavaScript>时所做的笔记(外加个人的深入研究)(幻灯片在这). 长久以来,面向对象在 Jav

JavaScript ES6函数式编程(二):柯里化、偏应用、组合、管道

上一篇介绍了闭包和高阶函数,这是函数式编程的基础核心.这一篇来看看高阶函数的实战场景. 首先强调两点: 注意闭包的生成位置,清楚作用域链,知道闭包生成后缓存了哪些变量 高阶函数思想:以变量作用域作为根基,以闭包为工具来实现各种功能 柯里化(curry) 定义:柯里化是把一个多参数函数转换为一个嵌套的一元函数的过程. 先看个简单的例子,这是一个名为 add 的函数:const add = (x, y) => x + y;调用该函数 add(1, 1).add(1, 2).add(1, 3)...很

[连载]Swift开发入门(06)--- 函数式编程

??面向对象编程和函数式编程是目前最主流的两种编程范式,而关于这两种范式孰优孰劣的讨论一直都没有停止过.事实上,真正理解两种编程范式的程序员不会武断的说这二者孰优孰劣,因为任何编程语言都没有什么灵丹妙药让其使用者成为优秀的程序员.其实,像Java这样很经典的面向对象的编程语言,也能够看到函数式编程的影子,如果你使用过访问者模式.命令模式,如果你使用过接口回调,你实际上已经使用了函数式编程的理念,而且在新版本的Java中,已经开始支持Lambda表达式和函数式接口,这些都是Java为了支持函数式编

JavaScript ES6函数式编程(三):函子

前面二篇学习了函数式编程的基本概念和常见用法.今天,我们来学习函数式编程的最后一个概念--函子(Functor). 相信有一部分同学对这个概念很陌生,毕竟现在已经有很多成熟的轮子,基本能满足我们日常的业务开发,所以没必须重复造轮子.但是,作为一名(未来)优秀的程序员,光会用怎么能行呢?必须要理解更深层的思想.下面就来学习函子部分的知识... 函子(Functor) 在正式学习函子之前,我会先抛出一个问题,先用普通的方式解决,然后转换为用函子解决,这能帮助我们更好的理解函子.同时,这也是我想说的,