Table of Contents
Preface
这是一本你可能2小时就能看完的小书,但是涵盖了基本所有函数式编程的内容,还包含了一些 ECMAScript 6 定义的函数式新特性, 如箭头函数, 模式匹配等等. 还会介绍函数式一些重要概念在 JavaScript是如何实现即应用, 以及如何以函数式的思想编写 JavaScript 代码.
对果JavaScript的基本概念对你来说:
可能本书并不适合你, 请先移步 JavaScript Allonge, 但是如果你学习函数式编程时感到:
那么这本书将会对你会有所帮助.
我选用的 JavaScript 函数式库是 Eweda(Ramda 的最初实现,更遵守函数式教条,但由于 javascript 的栈很容易爆,Ramda的实现要更 pratical 一些而且可以用的产品代码中, 千万不要在产品中用 eweda,这里只用eweda做介绍目的)
为什么不用 Underscore 请移步第二章
由于会介绍 ECMAScript 6 的新特性, 书中很多写法都是 ECMAScript 6 标准, 只能在实现这些 feature 的浏览器(如 Firefox, 请目前参照各浏览器的实现情况) 里运行. 另外, 大多数的例子源码都会在文章里的 jsbin 链接里.
Lambda
为什么一开始就讲 lambda, 如果小时候玩过游戏”半条命”,那么你早都见过 lambda 了.
我从wikipedia里面粘出来了这么一段定义:
lambda包括一条变换规则(变量替换)和一条函数定义方式,Lambda演算之通用在于,任何一个可计算函数都能用这种形式来表达和求值。因而,它是等价于图灵机的.
好吧, 跟没解释一样. 简单来说lambda其实就是 x
到 y
的映射关系,
但在大部分支持函数式的编程语言中, 它等价于匿名函数. 被称为 lambda
表达式. 因为这些函数只需要用一次, 而且变换不复杂, 完全不需要命名.
匿名函数在程序中的作用是可以作为参数传给高阶函数1, 或者作为闭包被返回.
但是匿名函数并不是原本的 lambda 算子, 因为匿名函数也可以接受多个参数, 如
1
multiple
(
x
,
y
)
=
x
*
y
写成简单映射的形式, 把名字去掉
1
(
x
,
y
)
->
x
*
y
这就是 lambda 了吗, 不是, lambda的用意是简化这个映射关系以至不需要名字, 更重要的是只映射一个 x.
什么意思呢? 让我们来分解一下上面的这个映射的过程.
- lambda 接受第一个参数
5
, 返回另一个 lambda1
(
5
)
->
(
y
->
5
*
y
)
- 该返回的 lambda
y -> 5*y
接收y
并且返回5*y
, 若在用4
调用该 lambda
1
4
->
5
*
4
因此这里的匿名函数 (x,y)->x*y
看似一个 lambda, 其实是两个 lambda
的结合.
而这种接受一个参数返回另一个接收第二个参数的函数叫柯里化2.
这里我们先忍一忍, 来看下 JavaScript 中的 lambda 表达式.
箭头函数(arrow function)
来看看越来越函数式の JavaScript
新的草案ECMAScript 6 (虽然说是草案,但你可以看到 Firefox 其实已经实现大部分的 feature)里我们越来越近了, 借助一下transcompiler例如Babel 我们完全可以在项目中开始使用es6了3。
看看里面有一行 arrow function,为什么叫箭头函数,还记得前面说lambda是提到的箭头吗。而且如果你之前用过 Haskell(单箭头) 或者Scala(双箭头), 会发现都用的是箭头来表示简单映射关系.
由于 arrow function 只在Firefox 22以上版本实现, 本节的所有代码都可以在Firefox的Console中调试, 其他chrome 什么的都没有实现(完全)4. 另外每节的最后我都会给出完整代码的可执行的 jsbin 链接.
声明一个箭头函数
你可以用两种方式定义一个箭头函数
1
([
param
]
[,
param
])
=>
{
2
statements
3
}
4
// or
5
param
=>
expression
单个表达式可以写成一行, 而多行语句则需要 block {}
括起来.
为什么要用箭头函数
看看旧的匿名函数怎么写一个使数组中数字都乘2的函数.
1
var
a
=
[
1
,
2
,
3
,
4
,
5
]
;
2
a
.
map
(
function
(
x
){
return
x
*
2
});
用箭头函数会变成
1
a
.
map
(
x
=>
x
*
2
);
只是少了 function
和 return
以及 block, 不是吗? 如果觉得差不多,
因为你看惯了 JavaScript 的匿名函数,
你的大脑编译器自动的忽略了,因为他们不需要显示的存在.
而 map(x => x*2)
要更 make sense,
因为我们需要的匿名函数只需要做一件事情, 我们需要的是 一个函数 f
,
可以将给定 x
, 映射到 y
. 翻译这句话的最简单的方式不就是 f
(x
> x*2)
Lexical this
如果你觉得这还不足以说服改变匿名函数的写法,
那么想想以前写匿名函数中的经常需要 var self=this
的苦恼.
1
var
Multipler
=
function
(
inc
){
2
this
.
inc
=
inc
;
3
}
4
Multipler
.
prototype
.
multiple
=
function
(
numbers
){
5
return
numbers
.
map
(
function
(
number
){
6
return
this
.
inc
*
number
;
7
})
8
}
9
new
Multipler
(
2
).
multiple
(
[
1
,
2
,
3
,
4
]
)
10
// =>
[
NaN
,
NaN
,
NaN
,
NaN
]
不 work, 因为 map 里面的 this 指向的是全局变量( window)
11
12
Multipler
.
prototype
.
multiple
=
function
(
numbers
){
13
var
self
=
this
;
// 保持 Multipler 的 this 的缓存
14
return
numbers
.
map
(
function
(
number
){
15
return
self
.
inc
*
number
;
16
})
17
}
18
new
Multipler
(
2
).
multiple
(
[
1
,
2
,
3
,
4
]
)
// =>
[
2
,
4
,
6
,
8
]
很怪不是吗, 确实是 Javascript 的一个 bug, 因此经常出现在各种面试题中.
试试替换成 arrow function
1
Multipler
.
prototype
.
multiple
=
function
(
numbers
){
2
return
numbers
.
map
((
number
)
=>
number
*
this
.
inc
);
3
};
4
5
console
.
log
(
new
Multipler
(
2
).
multiple
(
[
1
,
2
,
3
,
4
]
));
// =>
[
2
,
4
,
6
,
8
]
不需要 var self=this
了是不是很开心☺️现在, arrow function 里面的 this
会自动 capture 外层函数的 this
值.
JavaScript的匿名函数(anonymous function)
支持匿名函数, 也就意味着函数可以作为一等公民. 可以被当做参数, 也可以被当做返回值.因此, JavaScript 的支持一等函数的函数式语言, 而且定义一个匿名函数式如此简单.
创建一个匿名函数
在JavaScript里创建一个函数是如此的 ~~简单~~ … 比如:
1
function
(
x
){
2
return
x
*
x
;
3
}
// => SyntaxError: function statement requires a name
但是, 为什么报错了这里. 因为创建一个匿名函数需要用表达式(function expression). 表达式是会返回值的:
1
var
a
=
new
Array
()
// new Array 是表达式, 而这整行叫语句 statement
但为什么说 function statement requires a name
. 因为 JavaScript
还有一种 创建函数的方法–/function statement/.
而在上面这种写法会被认为是一个 function 语句, 因为并没有期待值. 而
function 语句声明是需要名字的.
简单将这个函数赋给一个变量或当参数传都不会报错, 因为这时他没有歧义,只能是表达式.比如:
1
var
squareA
=
function
(
x
){
2
return
x
*
x
;
3
}
但是这里比较 tricky 的是这下 squareA
其实是一个具名函数了.
1
console
.
log
(
squareA
)
// => function squareA()
虽然结果是具名函数,但是过程却与下面这种声明的方式不一样.
1
function
squareB
(
x
){
2
return
x
*
x
;
3
}
// => undefined
squareB
用的是 function statement 直接声明(显然 statement 没有返回),
而 squareA
则是先用 function expression 创建一个匿名函数,
然后将返回的函数赋给了名为 squareA
的变量. 因为表达式是有返回的:
1
console
.
log
(
function
(
x
){
return
x
*
x
});
2
// => undefined
3
// => function ()
第一个 undefined
是 console.log
的返回值, 因此 function()
则是打印出来的 function 表达式创建的匿名函数.
使用匿名函数
JavaScript 的函数是一等函数. 这意味着我们的函数跟值的待遇是一样的,于是它
可以赋给变量:
1
var
square
=
function
(
x
)
{
return
x
*
x
}
可以当参数, 如刚才见到的:
1
console
.
log
(
function
(
x
){
return
x
*
x
})
将函数传给了 console.log
可以被返回:
1
function
multiply
(
x
){
2
return
function
(
y
){
3
return
x
*
y
;
4
}
5
}
6
multiply
(
1
)(
2
)
// => 2
- 第二章会详细解释高阶函数和闭包.↩
- 柯里化会在第二章详细讨论.↩
- 可以看看es6比较有意思的新特性 http://blog.oyanglul.us/javascript/essential-ecmascript6.html↩
- Chrome有一个 feature toggle 可以打开部分 es6 功能 <chrome://flags/#enable-javascript-harmony>↩
高阶函数(Higher-order function)
我们已经见识到了匿名函数和箭头函数的用法, 匿名的一等函数到底有什么用呢? 来看看高阶函数的应用.
高阶函数意思是它接收另一个函数作为参数. 为什么叫 高阶:
来看看这个函数 f(x, y)
x(y)= 按照 lambda 的简化过程则是
1
f
(
x
)
=>
(
y
->
x
(
y
))
2
(
y
)
=>
x
(
y
)
可以卡出来虽然只是调用 f, 但是返回还有一个函数x要求值.
还记得高等数学里面的导数吗, 两阶以上的导数叫高阶导数. 因为求导一次以后返回的可以求导.
概念是一样的, 如同俄罗斯套娃 当函数执行以后还需执行或者要对参数执行, 因此叫高阶函数.
高阶函数很多典型的应用如 map
, reduce
.
他们都是以传入不同的函数来以不同的方式操作数组元素.
而柯里化, 则是每次消费一个参数并返回一个逐步被配置好的函数.
高阶函数的这些应用都是为函数的组合提供灵活性. 在本章结束相信你会很好的体会到函数组合的强大之处.
Higher-order function
在 JavaScript 中, 使用高阶函数是非常方便的.
函数作为参数
假设我现在要对一个数组排序, 用我们熟悉的 sort
1
[
1
,
3
,
2
,
5
,
4
].
sort
(
(
x
,
y
)
=>
x
-
y
)
如果我们要逆序的排序, 把减号左右的 x
和 y
呼唤,就这么简单,
但如果我是一个对象数组, 要根据对象的 id
排序:
1
[{
id
:
1
,
name
:
'
one
'
},
2
{
id
:
3
,
name
:
'
three
'
},
3
{
id
:
2
,
name
:
'
two
'
},
4
{
id
:
5
,
name
:
'
five
'
},
5
{
id
:
4
,
name
:
'
four
'
}].
sort
((
x
,
y
)
=>
x
.
id
-
y
.
id
)
是不是已经能够感受到高阶函数与匿名函数组合的灵活性.
函数作为返回值
函数的返回值可以不只是值, 同样也可以是一个函数, 来看 Eweda
内部的一个工具函数 aliasFor
, 他的作用是给函数 E 的一些方法起一些别名:
Note 听起来很怪不是吗, 函数怎么有方法, 实际上 JavaScript 的
function
是一个特殊 对象, 试试在 Firefox console 里敲console.log.
是不是看到了一些方法, 但是typeof console.log
是 function
1
var
E
=
()
=>
{}
2
var
aliasFor
=
oldName
=>
{
3
var
fn
=
newName
=>
{
4
E
[
newName
]
=
E
[
oldName
];
5
return
fn
;
6
};
7
return
(
fn
.
is
=
fn
.
are
=
fn
.
and
=
fn
);
8
};
这里有两个 return
, 一个是 fn
返回自己, 另一个是 aliasFor
也返回
fn
, 并且给 fn
了几个别名 fn.is
fn.are
…
什么意思呢? fn
返回 fn
. 很简单就是 fn() => fn
, 那么
fn()()=>fn()=>fn
…以此类推, 无论调用 fn 多少次,都最终返回 fn.
这到底有什么用呢, 由于这里使用了 fn
的副作用(side affect)
来干了一些事情 E[newName]=E[oldName]
, 也就是给 E 的方法起一个别名,
因此每次调用 fn 都会给 E
起一个别名. aliasFor
最后返回的是 fn
自己的一些别名, 使得可以 chain 起来更可读一些:
1
aliasFor
(
'
reduce
'
).
is
(
'
reduceLeft
'
).
is
(
'
foldl
'
)
另外, 函数作为返回值的重要应用, 柯里化与闭包, 将会在在后面专门介绍. 我们先来看下以函数作为参数的高阶函数的典型应用.
柯里化 currying
还记得 Haskell Curry吗
多巧啊, 人家姓 Curry 名 Haskell, 难怪 Haskell 语言会自动柯里化, 呵呵. 但是不奇怪吗, 为什么要柯里化呢. 为什么如此重要得让 Haskell 会默认自动柯里化所有函数, 不就是返回一个部分配置好的函数吗.
我们来看一个 Haskell 的代码.
1
max
3
4
2
(
max
3
)
4
结果都是4, 这有什么用呢.
这里看不出来, 放到 高阶函数
试试. 什么? 看不懂天书 Haskell, 来看看
JavaScript 吧.
我们来看一个问题
1. 写一个函数, 可以连接字符数组, 如 f(['1','2']) => '12'
好吧,如果不用柯里化, 怎么写? 啊哈 reduce
1
var
concatArray
=
function
(
chars
){
2
return
chars
.
reduce
(
function
(
a
,
b
){
3
return
a
.
concat
(
b
);
4
});
5
}
6
concat
(
[
'1'
,
'2'
,
'3'
]
)
// => '123'
很简单,对吧.
2. 现在我要其中所有数字加1, 然后在连接
1
var
concatArray
=
function
(
chars
,
inc
){
2
return
chars
.
map
(
function
(
char
){
3
return
(
+
char
)
+
inc
+
''
;
4
}).
reduce
(
function
(
a
,
b
){
5
return
a
.
concat
(
b
)
6
});
7
}
8
console
.
log
(
concatArray
(
[
'1'
,
'2'
,
'3'
]
,
1
))
// => '234'
3. 所有数字乘以2, 再重构试试看
1
var
multiple
=
function
(
a
,
b
){
2
return
+
a
*
b
+
''
3
}
4
var
concatArray
=
function
(
chars
,
inc
){
5
return
chars
.
map
(
function
(
char
){
6
return
multiple
(
char
,
inc
);
7
}).
reduce
(
function
(
a
,
b
){
8
return
a
.
concat
(
b
)
9
});
10
}
11
console
.
log
(
concatArray
(
[
'1'
,
'2'
,
'3'
]
,
2
))
// => '246'
是不是已经看出问题了呢? 如果我在需要每个数字都减2,是不是很麻烦呢.需要将
map
参数匿名函数中的 multiple 函数换掉. 这样一来 concatArray
就不能同时处理加, 乘和减? 那么怎么能把他提取出来呢?
来对比下柯里化的解法.
柯里化函数接口
1
var
multiple
=
function
(
a
){
2
return
function
(
b
){
3
return
+
b
*
a
+
''
4
}
5
}
6
7
var
plus
=
function
(
a
){
8
return
function
(
b
){
9
return
(
+
b
)
+
a
+
''
10
}
11
}
12
var
concatArray
=
function
(
chars
,
stylishChar
){
13
return
chars
.
map
(
stylishChar
)
14
.
reduce
(
function
(
a
,
b
){
15
return
a
.
concat
(
b
)
16
});
17
}
18
console
.
log
(
concatArray
(
[
'1'
,
'2'
,
'3'
]
,
multiple
(
2
)))
19
console
.
log
(
concatArray
(
[
'1'
,
'2'
,
'3'
]
,
plus
(
2
)))
有什么不一样呢 1. 处理数组中字符的函数被提取出来, 作为参数传入 2. 提取成柯里化的函数, 部分配置好后传入, 好处显而易见, 这下接口非常通畅 无论是外层调用
1
concatArray
([
'1'
,
'2'
,
'3'
],
multiple
(
2
))
还是内部的 map 函数
1
chars
.
map
(
stylishChar
)
这些接口都清晰了很多, 不是吗
这就是函数式的思想, 用已有的函数组合出新的函数, 而柯里化每消费一个参数, 都会返回一个新的部分配置的函数, 这为函数组合提供了更灵活的手段, 并且使得接口更为流畅.
自动柯里化
在 Haskell 语言中, 函数是会自动柯里化的:
1
max
3
4
其实就是
1
(
max
3
)
4
可以看看 max
与 max 3
函数的 类型
1
ghci
>
:
t
max
2
max
::
Ord
a
=>
a
->
a
->
a
看明白了么, Ord a =>
表示类型约束为可以比较大小的类型, 因此=max=
的类型可以翻译成: 当给定一个=a=, 会得到=a -> a=, 再看看=max
3=的类型就好理解了
1
ghci
>
:
t
max
3
2
(
Num
a
,
Ord
a
)
=>
a
->
a
左侧表示类型约束 a
可以是 Ord
或者 Num
, 意思是 max 3
还是一个函数,如果给定一个 Ord
或者 Num
类型的参数 则返回一个 Ord
或者 Num
.
现在是不是清晰了, 在 Haskell 中每给定一个参数, 函数如果是多参数的, 该函数还会返回一个处理余下参数的函数. 这就是自动柯里化.
而在 Javascript(以及大多数语言) 中不是的, 如果给定多参函数的部分参数,
函数会默认其他参数是 undefined
, 而不会返回处理剩余参数的函数.
1
function
willNotCurry
(
a
,
b
,
c
)
{
2
console
.
log
(
a
,
b
,
c
)
3
return
a
*
b
-
c
;
4
}
5
willNotCurry
(
1
)
6
// => NaN
7
// => 1 undefined undefined
如果使用自动柯里化的库 eweda, 前面的例子简直就完美了
1
var
multiple
=
curry
(
function
(
a
,
b
){
2
return
+
b
*
a
+
''
3
})
4
var
plus
=
curry
(
function
(
a
,
b
){
5
return
(
+
b
)
+
a
+
''
6
})
函数组合 function composition
通过前面介绍的高阶函数, map, fold 以及柯里化, 其实已经见识到什么是函数组合了. 如之前例子中的 map 就是 由 fold 函数与 reverse 函数组合出来的.
这就是函数式的思想, 不断地用已有函数, 来组合出新的函数.
如图就是函数组合,来自 Catgory
Theory(Funtor
也是从这来的,后面会讲到), 既然从 A到B 有对应的映射f,B到
C有对应的映射g, 那么 (g.f)(x)
也就是 f
与 g
的组合 g(f(x))
就是
A到 C 的映射。上一章实现的 map 函数就相当于 reverse.fold
.
pipe
类似 compose, eweda/ramda 还有一个方法叫 pipe, pipe 的函数执行方向刚好与
compose 相反. 比如 pipe(f, g)
, f
会先执行, 然后结果传给 g
,
是不是让你想起了 bash 的 pipe
1
find
/
|
grep
porno
实际上就是 pipe(find, grep(porno))(/)
没错,他们都是一个意思. 而且这个函数执行的方向更适合人脑编译(可读)一些.
如果你已经习惯 underscore 的这种写法
1
_
(
data
)
2
.
chain
()
3
.
map
(
data1
,
fn1
)
4
.
filter
(
data2
,
fn2
)
5
.
value
()
那么转换成 pipe 是很容易的一件事情,而且更简单明了易于重用和组合。
1
pipe
(
2
map
(
fn1
),
3
filter
(
fn2
)
4
)(
data
)