引言&前言
本书主要探索函数式编程[1](FP)的核心思想。在此过程中,作者不会执着于使用大量复杂的概念来进行诠释,这也是本书的特别之处。
这并不是说,各种复杂繁琐的概念是无意义的,更不是说,函数式编程者滥用了它们。一旦你完全掌握了轻量的函数式编程内容,你将会/但愿会想要对函数式编程的各种概念进行更正式更系统的学习。
单子是自函子范畴上的一个幺半群
本书 尽可能少用晦涩难懂的专业术语。我们将尝试以更实用的方法来探讨函数式编程,而非纯粹的学术角度。毫无疑问,肯定会有专业术语。但是我将会小心谨慎的引入这些术语并解释为何它们如此重要。
第1章:为什么使用函数式编程?
置信度
信任是什么意思?信任是指你通过读代码,不仅是跑代码,就能理解这段代码能干什么事,而不只是停留在它可能是干什么的层面。
通过读代码就能对我们的程序更有信心,我相信函数式编程技术的基础构成,是本着这种心态设计的
交流渠道
代码的主要作用是方便人与人交流。
我们的大部分时间其实都是在维护别人的代码(或自己的老代码),只有少部分时间是在敲新代码。
FP可以做到当我们读取一段代码时,我们将花费更少的时间来进行定位。我们的重点将在于如何组建所有已知的“乐高片段”,而不是这些“乐高片段”是什么意思。
可读性曲线

接受
一项技术你怎么称呼它不重要,重要的是理解它是什么并且它是怎么工作的。这并不是说共享术语不重要,它无疑可以简化经验丰富的专业人士之间的交流。但对学习者来说,它有点分散人的注意力。
形式主义总是阻碍我们前进。
你不需要它
YAGNI - You Ain’t Gonna Need It
你掌握了函数式编程并不意味着你一定得用它。此外,解决问题的方法很多,即使你掌握了更精炼的方法,能对维护和可扩展性更”经得起未来的考验”,但更轻量的函数式编程模式可能更适合该场景。
总结
这就是 JavaScript 轻量级函数式编程。我们的目标是学会与代码交流,而不是在符号或术语的大山下被压的喘不过气。希望这本书能开启你的旅程!
第2章:函数基础
什么是函数
简要的数学回顾

在数学中,函数总是获取一些输入值,然后给出一个输出值。你能听到一个函数式编程的术语叫做“态射”:这是一个优雅的方式来描述一组值和另一组值的映射关系,就像一个函数的输入值与输出值之间的关联关系。
函数 vs 程序
程序就是一个任意的功能集合。它或许有许多个输入值,或许没有。它或许有一个输出值( return 值),或许没有。
而函数则是接收输入值,并明确地 return 值。
函数输入
arguments 是你输入的值(实参), parameters 是函数中的命名变量(形参),用于接收函数的输入值
输入计数
这里有一个特殊的术语:Arity。Arity 指的是一个函数声明的形参数量。
随着输入而变化的函数
警告: 要对方便的诱惑有警惕之心。因为你可以通过这种方式设计一个函数,即使可以立即使用,但这个设计的长期成本可能会让你后悔。
函数输出
是否需要避免函数有可重构的多个输出?或许将这个函数分为两个或更多个更小的单用途函数。有时会需要这么做,有时可能不需要,但你应该至少考虑一下。
提前return
我认为在许多可读性的问题上,是因为我们不仅使用 return 返回不同的值,更把它作为一个流控制结构——在某些情况下可以提前退出一个函数的执行。我们显然有更好的方法来编写流控制( if 逻辑等),也有办法使输出路径更加明显
我不是说,你只能有一个 return,或你不应该提早 return,我只是认为在定义函数时,最好不要用 return 来实现流控制,这样会创造更多的隐含意义。尝试找出最明确的表达逻辑的方式,这往往是最好的办法
未 return 的输出
- 修改全局变量
- 副作用修改了引用的对象
隐式函数输出在函数式编程中有一个特殊的名称:副作用。当然,没有副作用的函数也有一个特殊的名称:纯函数。
函数功能
将其他函数视为值的函数是高阶函数的定义。函数式编程者们应该学会这样写!
保持作用域
我们将在本书的后续中大量使用闭包。如果抛开整个编程来说,它可能是所有函数式编程中最重要的基础。
句法
什么是名称?
匿名函数通常显示为:(anonymous function)。
在 ES6 中,匿名表达式可以通过名称引用来获得名称。
var x = function(){};x.name; // x
我有许多个理由可以解释命名函数比匿名函数更可取。事实上,我甚至认为匿名函数都是不可取的。相比命名函数,他们没有任何优势。
没有 function 的函数
虽然我不喜欢在我的应用程序中使用 =>,但我们将在本书的其余部分多次使用它,特别是当我们介绍典型的函数式编程实战时,它能简化、优化代码片段中的空间。不过,增强或减弱代码的可读性也取决你自己做的决定。
来说说 This ?
this 因为各种原因,不符合函数式编程的原则。其中一个明显的问题是隐式 this 共享。
从我的角度来看,问题不在于使用对象来进行操作,而是我们试图使用隐式输入取代显式输入。当我戴上名为函数式编程的帽子时,我应该把 this 放回衣架上。
总结
函数是强大的。
现在,让我们清楚地理解什么是函数:它不仅仅是一个语句或者操作的集合,而且需要一个或多个输入(理想情况下只需一个!)和一个输出。
函数内部的函数可以取到闭包外部变量,并记住它们以备日后使用。这是所有程序设计中最重要的概念之一,也是函数式编程的基础。
要警惕匿名函数,特别是 => 箭头函数。虽然在编程时用起来很方便,但是会对增加代码阅读的负担。我们学习函数式编程的全部理由是为了书写更具可读性的代码,所以不要赶时髦去用匿名函数。
别用 this 敏感的函数。这不需要理由。
第3章:管理函数的输入
立即传参和稍后传参
function ajax(url,data,callback) {// ..}
想象一个场景,你要发起多个已知 URL 的 API 请求,但这些请求的数据和处理响应信息的回调函数要稍后才能知道。
function getPerson(data,cb) {ajax( "http://some.api/person", data, cb );}function getOrder(data,cb) {ajax( "http://some.api/order", data, cb );}
手动指定这些外层函数当然是完全有可能的,但这可能会变得冗长乏味,特别是不同的预设实参还会变化的时候。
用一句话来说明发生的事情:getOrder(data,cb) 是 ajax(url,data,cb) 函数的偏函数(partially-applied functions)。
**
让我们定义一个 partial(..) 实用函数:
function partial(fn,...presetArgs) {return function partiallyApplied(...laterArgs){return fn( ...presetArgs, ...laterArgs );};}
bind(..)
var getPerson = ajax.bind( null, "http://some.api/person" );
那个 null 只会给我带来无尽的烦恼。
将实参顺序颠倒
function reverseArgs(fn) {return function argsReversed(...args){return fn( ...args.reverse() );};}
一次传一个
柯里化(currying)技术, 该技术将一个期望接收多个实参的函数拆解成连续的链式函数(chained functions),每个链式函数接收单一实参(实参个数:1)并返回另一个接收下一个实参的函数
function curry(fn,arity = fn.length) {return (function nextCurried(prevArgs){return function curried(nextArg){var args = prevArgs.concat( [nextArg] );if (args.length >= arity) {return fn( ...args );}else {return nextCurried( args );}};})( [] );}
我们用 curry(..) 函数来实现此前的 ajax(..) 例子:
var curriedAjax = curry( ajax );var personFetcher = curriedAjax( "http://some.api/person" );var getCurrentUser = personFetcher( { user: CURRENT_USER_ID } );getCurrentUser( function foundUser(user){ /* .. */ } );
柯里化和偏应用有什么用?
柯里化风格(sum(1)(2)(3))还是偏应用风格(partial(sum,1,2)(3))
- 使用柯里化和偏应用可以将指定分离实参的时机和地方独立开来(遍及代码的每一处),而传统函数调用则需要预先确定所有实参
- 当函数只有一个形参时,我们能够比较容易地组合它们
如何柯里化多个实参?
严格柯里化:一次只能传一个参数
松散柯里化: 允许你传入超过形参数量
function looseCurry(fn,arity = fn.length) {return (function nextCurried(prevArgs){return function curried(...nextArgs){var args = prevArgs.concat( nextArgs );if (args.length >= arity) {return fn( ...args );}else {return nextCurried( args );}};})( [] );}
反柯里化
将类似 f(1)(2)(3) 的函数变回类似 g(1,2,3) 的函数
function uncurry(fn) {return function uncurried(...args){var ret = fn;for (let i = 0; i < args.length; i++) {ret = ret( args[i] );}return ret;};}// ES6 箭头函数形式var uncurry =fn =>(...args) => {var ret = fn;for (let i = 0; i < args.length; i++) {ret = ret( args[i] );}return ret;};
请不要以为 uncurry(curry(f)) 和 f 函数的行为完全一样,如果你少传了实参,就会得到一个仍然在等待传入更多实参的部分柯里化函数
只要一个实参
实用函数
function unary(fn) {return function onlyOneArg(arg){return fn( arg );};}// ES6 箭头函数形式var unary =fn =>arg =>fn( arg );
传一个返回一个
function identity(v) {return v;}// ES6 箭头函数形式var identity =v =>v;var words = " Now is the time for all... ".split( /\s|\b/ );words;// ["","Now","is","the","time","for","all","...",""]words.filter( identity );
恒定参数
function constant(v) {return function value(){return v;};}// or the ES6 => formvar constant =v =>() =>v;
仔细思考下面,命名函数调用比匿名声明更易读和统一
p1.then( foo ).then( () => p2 ).then( bar );// 对比:p1.then( foo ).then( constant( p2 ) ).then( bar );
扩展在参数中的妙用
自动扩展数组到参数
function spreadArgs(fn) {return function spreadFn(argsArr) {return fn( ...argsArr );};}// ES6 箭头函数的形式:var spreadArgs =fn =>argsArr =>fn( ...argsArr );
相反的方法,聚合参数
function gatherArgs(fn) {return function gatheredFn(...argsArr) {return fn( argsArr );};}// ES6 箭头函数形式var gatherArgs =fn =>(...argsArr) =>fn( argsArr );
参数顺序的那些事儿
一般的参数都是有序数组这种,对于参数多的情况下难以通过下标控制
使用对象解构编程命名参数,来让代码更易读
function partialProps(fn,presetArgsObj) {return function partiallyApplied(laterArgsObj){return fn( Object.assign( {}, presetArgsObj, laterArgsObj ) );};}function curryProps(fn,arity = 1) {return (function nextCurried(prevArgsObj){return function curried(nextArgObj = {}){var [key] = Object.keys( nextArgObj );var allArgsObj = Object.assign( {}, prevArgsObj, { [key]: nextArgObj[key] } );if (Object.keys( allArgsObj ).length >= arity) {return fn( allArgsObj );}else {return nextCurried( allArgsObj );}};})( {} );}
使用方式
function foo({ x, y, z } = {}) {console.log( `x:${x} y:${y} z:${z}` );}var f1 = curryProps( foo, 3 );var f2 = partialProps( foo, { y: 2 } );f1( {y: 2} )( {x: 1} )( {z: 3} );// x:1 y:2 z:3f2( { z: 3, x: 1 } );// x:1 y:2 z:3
们不用再为参数顺序而烦恼了!👍!
属性扩展
对于不能改变函数签名的多参函数要咋整?
使用如下黑科技:
function spreadArgProps(fn,propOrder =fn.toString().replace( /^(?:(?:function.*\(([^]*?)\))|(?:([^\(\)]+?)\s*=>)|(?:\(([^]*?)\)\s*=>))[^]+$/, "$1$2$3" ).split( /\s*,\s*/ ).map( v => v.replace( /[=\s].*$/, "" ) )) {return function spreadFn(argsObj) {return fn( ...propOrder.map( k => argsObj[k] ) );};}
使用方式:
function bar(x,y,z) {console.log( `x:${x} y:${y} z:${z}` );}var f3 = curryProps( spreadArgProps( bar ), 3 );var f4 = partialProps( spreadArgProps( bar ), { y: 2 } );f3( {y: 2} )( {x: 1} )( {z: 3} );// x:1 y:2 z:3f4( { z: 3, x: 1 } );// x:1 y:2 z:3
👍!
无形参风格
not取否工具函数
function not(predicate) {return function negated(...args){return !predicate( ...args );};}// ES6 箭头函数形式var not =predicate =>(...args) =>!predicate( ...args );
when条件工具函数
function when(predicate,fn) {return function conditional(...args){if (predicate( ...args )) {return fn( ...args );}};}
综合例子:
function output(msg) {console.log( msg );}function isShortEnough(str) {return str.length <= 5;}var isLongEnough = not( isShortEnough );var printIf = uncurry( partialRight( when, output ) );var msg1 = "Hello";var msg2 = msg1 + " World";printIf( isShortEnough, msg1 ); // HelloprintIf( isShortEnough, msg2 );printIf( isLongEnough, msg1 );printIf( isLongEnough, msg2 ); // Hello World
无形参就是尽量将函数作为一等变量传递,实际参数发生落地的地方尽量往后推延
总结
- 偏应用是用来减少函数的参数数量 —— 一个函数期望接收的实参数量 —— 的技术,它减少参数数量的方式是创建一个预设了部分实参的新函数。
- 柯里化是偏应用的一种特殊形式,其参数数量降低为 1,这种形式包含一串连续的链式函数调用,每个调用接收一个实参。当这些链式调用指定了所有实参时,原函数就会拿到收集好的实参并执行。你同样可以将柯里化还原。其它类似
unary(..)、identity(..)以及constant(..)的重要函数操作,是函数式编程基础工具库的一部分。 - 无形参是一种书写代码的风格,这种风格移除了非必需的形参映射实参逻辑,其目的在于提高代码的可读性和可理解性。
第 4 章:组合函数
一个函数式编程者,会将他们程序中的每一个函数当成一小块简单的乐高部件。他们能一眼辨别出蓝色的 2x2 方块,并准确地知道它是如何工作的、能用它做些什么。当构建一个更大、更复杂的乐高模型时,当每一次需要下一块部件的时候,他们能够准确地从备用部件中找到这些部件并拿过来使用。
输出到输入

wordsUsed <— unique <— words <— text
制造机器

function compose2(fn2,fn1) {return function composed(origValue){return fn2( fn1( origValue ) );};}// ES6 箭头函数形式写法var compose2 =(fn2,fn1) =>origValue =>fn2( fn1( origValue ) );
组合的变体
**var** letters = compose2( words, unique );
重点是,函数的组合不总是单向的。有时候我们将灰方块放到蓝方块上,有时我们又会将蓝方块放到最上面。
假如糖果工厂尝试将包装好的糖果放入搅拌和冷却巧克力的机器,那他们最好要小心点了。
通用组合

“自动组合各种小机器,包装成一个更大更好的机器“
function compose(...fns) {return function composed(result){// 拷贝一份保存函数的数组var list = fns.slice();while (list.length > 0) {// 将最后一个函数从列表尾部拿出// 并执行它result = list.pop()( result );}return result;};}// ES6 箭头函数形式写法var compose =(...fns) =>result => {var list = fns.slice();while (list.length > 0) {// 将最后一个函数从列表尾部拿出// 并执行它result = list.pop()( result );}return result;};
不同的实现
- compose的饥渴模式
function compose(...fns) {return function composed(result){return fns.reverse().reduce( function reducer(result,fn){return fn( result );}, result );};}// ES6 箭头函数形式写法var compose = (...fns) =>result =>fns.reverse().reduce((result,fn) =>fn( result ), result);
- compose的懒惰模式
function compose(...fns) {return fns.reverse().reduce( function reducer(fn1,fn2){return function composed(...args){return fn2( fn1( ...args ) );};} );}// ES6 箭头函数形式写法var compose =(...fns) =>fns.reverse().reduce( (fn1,fn2) =>(...args) =>fn2( fn1( ...args ) ));
