引言&前言
本书主要探索函数式编程[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 => form
var 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:3
f2( { 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:3
f4( { 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 ); // Hello
printIf( 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 ) )
);