作者:欧阳继超
出版社:电子工业出版社
格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT
前端函数式攻城指南试读:
前言
1.看本书之前你要知道
1)最好能看懂JavaScript代码
这既不是一本介绍Clojure的书,也不是一本介绍JavaScript的书,而是一本介绍如何用 JavaScript 函数式编程的书。其中一些函数式的思想和表现形式都借用了Clojure,因此叫作 Clojure 风格的函数式 JavaScript,但是并不要求在读本书前会Clojure[1],而只需要能阅读JavaScript代码。如果你会Clojure,则可以完全忽略我解释Clojure代码的段落,当然JavaScript的部分才是重点。
2)你可能买错书了,如果你
● 想学JavaScript
这不是一本JavaScript的教科书,这里只会介绍如何用JavaScript进行函数式编程,所以如果想要系统地学习JavaScript,则学习《JavaScript语言精粹》可能已经足够了。另外,如果读者的英文水平好的话,则还有一本可以在线免费阅读的JavaScript Allonge。
● 想学Clojure
同样,这也不是一本 Clojure 的教科书,只含有一些用于阐述函数式编程思想
的Clojure代码。你确实可以学到一些Clojure编程知识,但很可能是非常零碎且不完整的知识。如果想要系统地了解和学习 Clojure,则非常推荐你阅读 The Joy of Clojure[2],另外,如果读者英文比较好,还有一本可以免费在线阅读的CLOJURE for the BRAVE and TRUE也非常不错。
● 是函数式编程的专家
如果你已经在日常工作或学习中使用Scala、Clojure或者Haskell等函数式语言编程的话,那么本书对你在函数式编程上的帮助不会太大。不过:这本书对缓解你从函数式语言迁移到 JavaScript 编程的不适应该是非常有效的。这也正是本书的目的之一。
2.准备环境
在开始阅读本书之前,如果你希望能运行书中的代码,可能需要一些环境的配置。而且书中的所有源码和运行方式都可以在本书的 Github 仓库[3]中找到。当然如果你使用Emacs(同时还配置了org babel的话)阅读本书的源码,对于大部分代码只需要光标放在代码处按c-cc-c即可。
● JavaScript
原生的JavaScript没有什么好准备的,可以通过Node或者Firefox(推荐)的Console 运行代码。当然第 5 章会有一些使用 Sweet.js 写的 Macro,这则需要安装Sweet.js。
-安装Node/io.js(1)下载Node.js。(2)如果使用Mac,可以直接用Brew安装。
-安装Sweet.js
在安装完Node.js之后在命令行输入:
● Clojure
书中的 Clojure 代码大都用来描述函数式编程的概念,当然如果想要运行书中的Clojure代码,首先需要安装JⅤM或者JDK[4],至少需要1.6版本,推荐安装1.8版本。
-安装leiningen
leiningen是Clojure的包管理工具,类似于Node的Npm、Ruby的bundle、Python的pip。另外leiningen还提供脚手架的功能。可以通过官网的脚本安装[5]。Mac用户可以简单地使用brew installleiningen安装。
安装完成之后,就可以运行leinrepl,打开repl,试试输入下列Clojure代码,你将会立马看见结果。
-编辑器
如果更喜欢使用编辑器来编辑更长的一段代码,我推荐非 Emacs 用户使用Light Table[6],Intellij用户则使用cursive[7]。当然如果读者已经在使用Emacs,那就更
完美了,Emacscider mode[8]是Clojure编程不错的选择。
3.本书中的代码
书中的所有源码和运行方式都可以在本书的 Github 仓库[9]中找到,书中几乎所有的例子都以测试的形式完成。
4.反馈9
如果你是开源贡献者,那么应该很习惯通过Github Issue提交任何反馈,如果是Pull Request,那就更好了。当然如果没有使用过Github Issue也没有关系,这里[10]有非常详细的教程。
5.代码风格约定
本书的JavaScript代码都遵循Airbnb JavaScript Style Guide[11]中的ES5和React的风格约定。
6.本书的组织结构
第1章
将介绍 JavaScript 的基本函数式背景,简要地介绍为什么要关心函数式编程,为什么说Underscore不够函数式,JavaScript要作为完整的函数式语言还缺些什么?
第2章
主要介绍 Clojure 的集合数据结构。这是个无聊但是又很重要的章节,可以说函数式编程最基本、最重要的就是集合操作。本章会涉及如何操作集合、惰性求值与惰性序列。
第3章
在了解了持久性数据结构后,我们可能会产生疑惑,如果数据结构都是不可变的,那么循环该怎么写呢?本章就是要解开各种使用不可变数据结构的疑惑,用这些不可变数据结构可以切换一种编程的思维方式。
第4章
Underscore并不利于函数组合,但是函数组合其实是函数式编程最重要的思想。在这一章里面,我会详细介绍为什么说Underscore错了,而为什么要喜欢上柯里化,以及Clojure 1.7新推出的Transducer又是如何帮助我们更容易组合出更高效的函数的。
第5章
我特别不情愿把 Macro 翻译成宏。宏特别容易让人以为是 C 语言里面那个#define宏,虽然都是宏,但其实那里跟这里说的Macro不是一个级别的。Macro是Lisp语言之所以特别的原因之一。本章我们就来看看到底什么是、为什么,以及如何在JavaScript中使用Macro。
第6章
这里说的模式匹配包括两种:一种是按位置或者key匹配集合,取出相应数据。另一种是 Haskell 风格的对函数参数的模式匹配。本章篇幅比较小,因为模式匹配并不是 Clojure(也不是 JavaScript)的主要模式,尽管在一些有强大类型系统的函数式语言(Scala、Haskell)中比较重要。
第7章
Monad这个范畴论里出来的神秘玩意,但你可能没有在意,其实这在前端世界早都被玩腻了。本章将会介绍Monad和它的朋友们,并且将带你体验JavaScript的Promise,以及Reactive编程。
第8章
并发编程一直是令人头疼的编程方式,直到Clojure和Go的出现,彻底改变了我们并发编程的方式。而对于单线程的 JavaScript,基于事件循环的并发模型也一直困扰着我们,到底能从 Clojure 学些什么,可以使我们的前端并发编程之路更顺畅一些呢?本章将带你熟悉并发、JavaScript的并发模型,以及CSP并发模型。
7.本书使用的约定
本书使用以下字体排版约定。
1)楷体
表示新的术语。
2)等宽字体
代码清单,出现在段落之内则表示变量、函数名、关键字等。
3)粗体
重点概念。
4)下画线
需要填入的词,我可能已经帮大家填上了。
5)横线
可以忽略的词。
[1] 就像计算机程序构造与解释中说的,Lisp 语言基本没有语法,就像学习象棋的规则只用花很少的时间,而如何下好棋,才是学习的关键,也是乐趣所在。
[2] 中文叫《Clojure编程乐趣》,但是只有第一版的,原书已经第二版了。我刚好有幸翻译了作者Michael Fogus的另一本书《JavaScript函数式编程》。
[3] https://github.com/jcouyang/clojure-flavored-javascript/tree/source.
[4] http://www.oracle.com/technetwork/java/javase/downloads/index.html.
[5] http://leiningen.org.
[6] http://lighttable.com/.
[7] https://cursive-ide.com/.
[8] https://github.com/clojure-emacs/cider.
[9] https://github.com/jcouyang/clojure-flavored-javascript/issues.
[10] https://guides.github.com/features/issues/.
[11] https://github.com/airbnb/javascript.第1章函数式JavaScript
本章将介绍JavaScript的函数式背景,包括:(1)为什么说JavaScript是函数式语言。(2)我们为什么要关心函数式编程。(3)为什么说Underscore不够函数式。(4)要作为完整的函数式语言,JavaScript还缺些什么。1.1JavaScript也是函数式语言吗
说到 JavaScript,可能大家的第一反应是它是一门面向对象的语言。事实上,JavaScript是基于原型(prototype-based)的多范式编程语言。也就是说面向对象只是JavaScript支持的其中一种范式而已,由于JavaScript的函数是一等公民,所以它也支持函数式编程范式。1.1.1 编程范式
常见的编程范式有3种:命令式、面向对象及函数式。事实上还有第4种,逻辑式编程,例如我们在大学时学过的C语言,就是标准的命令式语言。而如果你在大学自学过Java打过黑工的话,那么你对面向对象应该再熟悉不过了吧。可能大部分人(以为)接触函数式的机会比较少,因为它是更接近于数学和代数的一种编程范式。下面让我们分别看看这几种主要的编程范式,如图1-1所示。图1-1 主要的编程范式
● 命令式
这恐怕是我们最熟悉的编程范式了(大部分计算机课程都会是 C 语言的),命令式顾名思义就是以一条条命令的方式编程,告诉计算机我需要先做这个任务,然后做另一个任务。还有一些控制命令执行过程的流控制,比如我们熟悉的循环语句:
当然还有分支语句、switch等,都用来控制命令的执行过程。
● 函数式
函数式则更接近于数学,简单来说就是对表达式求值。跟面向对象有所不同的是函数式对问题的抽象方式是抽象成带有动作的函数。其思维更像我们小时候解应用题时需要套用各种公式来求解的感觉。当然函数式跟面向对象一样还包含了很多概念,比如高阶函数、不可变性、惰性求值等。
● 面向对象
这恐怕是目前最常见的编程范式了,绝大部分工程项目使用的语言都是面向对象的。而面向对象的思想则更接近于现实世界,封装好的对象之间通过消息互相传递信息,以这种熟悉的方式来建模显然要更容易一点。面向对象由一些我们熟悉的概念组成,比如封装、继承、多态等。而面向对象的思维主要是通过抽象成包含状态和一些方法的对象来解决问题,可以通过继承关系复用一些方法和行为。
● 逻辑式编程
可能这个名词我们听得比较少,我们经常在用却没有意识到的SQL的query语句就是逻辑式编程。所谓逻辑式,就是通过提问找到答案的编程方式。比如:
这里问了两个问题:(1)性别是女?(2)名字必须是“连顺”或者“女神”?
那么得到的答案就是符合问题描述的结果集了。
除了最常见的SQL,Clojure也提供了core.logic的库来方便使用者进行逻辑式编程[1]。1.1.2 JavaScript的函数式支持
说了这么多种编程范式,JavaScript对函数式的支持到底如何呢?
首先,如果语言中的函数不是一等公民,那么它跟函数式编程也就基本划清界限了。比如Java 8之前的版本,值和对象才是一等公民,要写一个高阶函数可能还需要把函数包在对象中才行[2]。
幸好 JavaScript 中的函数是一等函数,所谓一等,就是说跟值一样都是一等公民,所有值能到的地方都可以替换成函数。例如,可以跟值一样作为别的函数的参数,可以被别的函数像值一样返回,而这个“别的函数”叫作高阶函数。
● 函数作为参数
函数作为参数最典型的应用要数map了,想必如果没有使用过Underscore,也或多或少会用过ECMAScript5中Array的map方法吧。map可以简单地将一个数组转换为另一个数组。
可以看到函数function(x){return x++}作为参数被传入Array的map方法中。map是函数式编程最常见的标志性函数,想想在ECMAScript 5出来之前应该怎么做类似的事情:
这段命令式的代码跟利用map的函数式代码解决问题的方式和角度是完全不同的。命令式需要操心所有的过程、如何遍历以及如何组织结果数据。而map由于将遍历、操作及结果数据的组织过程封装至Array中,从而参数化了最核心过程。而这里的核心过程就是map的参数里的匿名函数中的过程,也是我们真正关心的主要逻辑。
● 函数作为返回值
函数作为返回值的用法可能在 JavaScript 中会更为常见。而且在不同场景下被返回的函数又有着不同的名字。
-柯里化
我们把一个多参的函数变成一次只能接受一个参数的函数的过程叫作柯里化。如:
柯里化这样做的目的非常简单,可以部分地配置函数,然后可以继续使用这些配置过的函数。当然,我会在第4章更详细地解释为什么要柯里化,在这之前闲不住的读者可以先猜猜为什么要把柯里化放在函数组合那一章。
-thunk
thunk(槽)[3]是指有一些操作不被立即执行,也就是说准备好一个函数,但是不执行,默默等待着合适的时候被合适的人调用。我实在想不出能比图1-2更能解释thunk的了。在第2章,你会见到如何用thunk实现惰性序列。
● 越来越函数式的ES6
ECMAScript 6[4]终于正式发布了,新的规范有非常多的新特性,其中不少借鉴自其他函数式语言的特性,给JavaScript语言添加了不少函数式的新特性。
虽然浏览器厂商都还没有完全实现ES6的所有规范,但是其实我们可以通过一些中间编译器使用大部分ES6的新特性,如下所述。图1-2 thunk像是—个封装好待执行的容器
Babel
这是目前支持ES6实现最多的编译器了,没有之一,主要是Facebook在维护,因此也可以编译Facebook的React。这也是目前[5]能实现尾递归优化的唯一编译器。不过关于尾递归只能优化尾子递归,相互递归的优化还没有实现。
Traceur
Google 出得比较早的一个老牌编译器,支持的 ES6 也不少了。但是从 Github上来看似乎已经没有Babel活跃了。
当然,除了这些也可以直接使用 Firefox。作为 ES6 规范的主要制定者之一的Mozilla出的Firefox当然也是浏览器中实现ES6标准最多的。
● 箭头函数
这是ES6发布的一个新特性,虽然Firefox支持已久了,不算什么新东西,但是标准化之后还是比较令人激动的。箭头函数也被叫作肥箭头(Fat Arrow)[6],大致是借鉴自CoffeeScript或者Scala语言。箭头函数是提供词法作用域的匿名函数。
-声明一个箭头函数
你可以通过两种方式定义一个箭头函数:
表达式可以省略块(Block)括号,而多行语句则需要用块括号括起来。
-为什么要用箭头函数
虽然看上去跟以前的匿名函数没有什么区别,我们可以对比旧的匿名函数是如何写一个使数组中数字都乘2的函数:
而使用箭头函数会变成:
使用箭头函数可以少写function和return及块括号,从而让我们更关心的转换关系变得更明显。略去没用的长的函数关键字,其实可以让代码更简捷、更可读。特别是在传入高阶函数作为参数的时候,map(x=>x*2)更形象和突出地表达了核心变换逻辑。
-词法绑定
如果你觉得这种简化的语法糖还不足以说服你改变匿名函数的写法,那么想想以前匿名函数中经常需要var self=this的苦恼吧。
● 第5行中使用self保持了指向Multipler实例的this引用的缓存。
● 第 7 行使用 self 引用 Multipler 的实例,而此时的 this 应该指向numbers的元素。
这样做很怪,不是吗?因此经常出现在各种面试题中,让你猜猜this到底是谁,或者让你去修正 this 绑定,方法如此之多。但是不管是使用 ECMAScript 5的bind,还是map的第三个参数来保证this的绑定不出错,都逃脱不了要手动修正this绑定的命运。
那么如果用箭头函数就不会存在上述问题:
newMultipler(2).multiple([1,2,3,4]);//=> [2,4,6,8]
猜猜这里的this的作用域是什么?现在,箭头函数里面的this的作用域是外层multiple函数,不会受到map运行时上下文的影响[7]。而是从词法上就能轻松确定this的绑定。不需要var self=this了是不是确实方便了许多呢?不仅不会再被各种怪异的面试题坑了,还让代码更容易推理了。
● 尾递归优化
Clojure能够通过recur函数对尾递归进行优化,但是ES5的JavaScript实现不会对尾递归进行任何优化,很容易出现爆栈的现象。而ES6的标准已经发布了对尾递归优化的支持,下来我们能做的只是等各大浏览器厂商的实现了。
不过在坐等原生实现的同时,我们也可以通过一些中间编译器如Babel,把ES6的代码编译成ES5标准的JavaScript,而在Babel编译的过程中就可以把尾递归优化成循环。
● Destructure
在解释Destructure[8]之前,先举个生动的例子,比如在吃奥利奥的时候,我的吃法是这样的。(1)掰成两片,一片是不带馅的,一片是带馅的。(2)带馅的一半沾一下牛奶。(3)舔掉中间夹心的馅。(4)合起来吃掉。
如果写成代码,则大致应该是这样的:
注意那个诡异的shift,如果用Destructure会写得稍微优雅一些:
有没有觉得我掰奥利奥的姿势变酷了许多?这就是Destructure,给定一个特定的模式[top,...middleAndButton],让数据["top","middle","bottom"]按照该模式匹配进来。同样,我将会专门在第6章介绍模式匹配这个概念,它是一些函数式语言例如Scala、Haskell 的核心所在。不过可以放心的是,你也不必在此之前先学习Scala或Haskell[9],我还是会用最流行的JavaScript来介绍模式匹配,如图1-3所示。图1-3 我觉得这个玩具可以特别形象地解释模式匹配这个概念1.2作为函数式语言,JavaScript还差些什么
作为多编程范式的语言,原型链支持的当然是面向对象编程,然而同时支持一等函数的 JavaScript 也给函数式编程带来了无限的可能。之所以说可能,是因为JavaScript在语言层面上对于函数式的支持还是非常局限的,为了让JavaScript全面支持函数式编程,还需要非常多的第三方库的支持。下面我们来看看 JavaScript 比起函数式语言,到底还差些什么。1.2.1 不可变数据结构
首先需要支持的当然是不可变(Immutable)的数据结构,这意味着任何操作都不会改变该数据结构的内容。JavaScript中除了原始类型,其他都是可变的(Mutable)。相反,Clojure的所有数据结构都是不可变的。
JavaScript一共有6种原始类型(包括ES6新添加的Symbol类型),它们分别是Boolean、Null、Undefined、Number、String和Symbol。除了这些原始类型,其他的都是Object,而Object都是可变的。
比如JavaScript的Array是可变的:
a的引用虽然没有变,但是内容发生了变化。
Clojure的Ⅴector类型则行为刚好相反:
它对a的操作并没有改变a的内容,而是conj操作返回的改变后的新列表。在接下来的第2章你将会看到Clojure是如何实现不可变的数据结构的。1.2.2 惰性求值
惰性(Lazy)指求值的过程并不会立刻发生。比如在解答一些数学题(特别是求极限的)时,我们可能不需要把所有表达式求值才能得到最终结果,在计算的过程中一些表达式能被消掉。所以惰性求值是相对于及早求值(Eager Evaluation)的。
大部分语言中,参数中的表达式都会被先求值,这也叫作应用序语言。比如下面的JavaScript函数:
getFirstName 与 getLastName 会依次执行,返回值作为 wholeNameOf函数的参数,wholeNameOf函数最后被调用。
另外,对于数组操作,大部分语言同样采用应用序。
这个表达式立刻会返回结果[1,2,3,4],就算这个表达式没有被用到。
当然这并不是说 JavaScript 语言使用应用序有问题,但是没有提供惰性序列的支持就是JavaScript的不对了。例如map一个含有一千个元素的列表后,发现其实只会用到前10个元素,去计算所有元素就显得多余了。1.2.3 函数组合
面向对象通常被比喻为名词,而函数式编程是动词。面向对象抽象的是对象,对于对象的描述自然是名词。面向对象把所有操作和数据都封装在对象内,通过接收消息做相应的操作。比如,对象Kitty和Pussy,它们可以接收“打招呼”的消息,然后做相应的动作。而函数式的抽象方式刚好相反,是把动作抽象出来,比如就是一个函数“打招呼”,而参数,则是作为数据传入的Kitty或者Pussy,是完全透明的。比如Kitty进入函数“打招呼”时,出来的应该是一只Hello Kitty。
面向对象可以通过继承和组合在对象之间分享一些行为或者属性,函数式的思路就是通过组合已有函数形成一个新的函数。JavaScript 语言虽然支持高阶函数,但是并没有一个原生的利于组合函数而产生新函数的方式。关于函数组合的技巧,会在第4章做详细的解释,而这些强大的函数组合方式却往往被类似underscore库的光芒掩盖掉。1.2.4 尾递归优化
Clojure的数据结构都是不可变的,除了使用数据结构本身的方法进行遍历,另外的循环手段自然只能是递归了。但是在没尾递归优化的 JavaScript 中就不会那么愉快了。
在JavaScript中可能会经常看到这样的代码:
如果使用Clojure,做类似的事情通常会使用reduce解决,代码会变成这样:
recur看起来跟for循环非常类似,其实它是尾递归,如果把loop写成一个函数:
事实上效果是一样的,但是如果把 recur 想象成 zipping-add,则明显能看出zipping-add是一个尾递归函数。
因此反过来看,若是要把尾递归换成循环是多么容易的一件事情,关键是需要让解释器识别尾递归。
但这不是 Clojure 的风格,也不是函数式的风格。递归应该被认为是比较低级别的操作,像这种操作还是应该优先使用较高级别的map、reduce来解决。
相比其他语言,Clojure的map是个神奇的函数,若是给多个向量,则它做的事情会相当于先zip成一个向量,再把向量的元素apply到组合子[10]上。这样完全不需要循环和变量,得到了一段不需要循环和变量的简洁且更具描述性的代码。但是,在写低级别的一些代码的时候,递归仍然是强有力的武器,而且尾递归优化能让我们同时使用递归而不失性能,在第5章会更详细地介绍不可变数据结构以及递归。1.3Underscore你错了
如果提到JavaScript的函数式库,则可能会联想到Underscore[11]。Underscore的官网解释是这样的:
Underscore提供了100多个函数,不仅有常见的函数式小助手:map、filter、invoke,还有更多的一些额外的好处……
我就懒得翻译完了,重点是这句话里面的“函数式小助手”,这点我实在不是很同意。1.3.1 跟大家都不一样的 map 函数
比如对于map这个函数式编程中比较常见的函数,我们来看看函数式语言中都是怎么做map的:
Clojure:
其中inc是一个给数字加一的函数。
Haskell:
同样,(1+)是一个函数,可以给数字进行加一操作。
这是非常简单的map操作,应用函数inc、(1+)到数组中的每一个元素。同样的事情我们试试用Underscore来实现一下:
感觉到有什么变化了吗?有没有发现参数的顺序完全不同了?好吧,你可能要说这并不是什么问题啊?不就是map的API设计得不太一样么?也没有必要保持所有语言的map都是一样的吧?
试读结束[说明:试读内容隐藏了图片]