函数式编程指北
# 1. 纯函数的好处
纯函数概念:纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。
在不纯的版本中,checkAge 的结果将取决于 minimum 这个可变变量的值。换句话说,它取决于系统状态(system state)。这一点令人沮丧,因为它引入了外部的环境,从而增加了认知负荷(cognitive load)。
// 不纯的
var minimum = 21
var checkAge = function(age) {
return age >= minimum
}
// 纯的
var checkAge = function(age) {
var minimum = 21
return age >= minimum
}
2
3
4
5
6
7
8
9
10
11
12
副作用:副作用是在计算结果的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互。
只要是跟函数外部环境发生的交互就都是副作用 —— 这一点可能会让你怀疑无副作用编程的可行性。函数式编程的哲学就是假定副作用是造成不正当行为的主要原因。这并不是说,要禁止使用一切副作用,而是说,要让它们在可控的范围内发生。
戏剧性的是:纯函数就是数学上的函数,而且是函数式编程的全部。使用这些纯函数编程能够带来大量的好处。
值得注意的一点是,可以通过延迟执行的方式把不纯的函数转换为纯函数。
这里有趣的地方在于我们并没有真正发送 http 请求——只是返回了一个函数,当调用它的时候才会发请求。这个函数之所以有资格成为纯函数,是因为它总是会根据相同的输入返回相同的输出:给定了 url 和 params 之后,它就只会返回同一个发送 http 请求的函数。
var pureHttpCall = memoize(function(url, params) {
return function() {
return $.getJSON(url, params)
}
})
2
3
4
5
追求“纯”的理由。
- 可缓存性(利用闭包):纯函数总能够根据输入来做缓存。
- 可移植性/自文档化:纯函数是完全自给自足的,它需要的所有东西都能轻易获得。
- 可测试性:纯函数让测试更加容易。只需简单地给函数一个输入,然后断言输出就好了。
- 合理性:很多人相信使用纯函数最大的好处是引用透明性。如果一段代码可以替换成它执行所得的结果,而且是在不改变整个程序行为的前提下替换的,那么我们就说这段代码是引用透明的。
# 2. 代码组合
- 这就是组合(compose)。f 和 g 都是函数,x 是在它们之间通过“管道”传输的值。
var compose = function(f, g) {
return function(x) {
return f(g(x))
}
}
2
3
4
5
pointfree 模式:函数无须提及将要操作的数据是什么样的。
pointfree 模式能够帮助我们减少不必要的命名,让代码保持简洁和通用。对函数式代码来说,pointfree 是非常好的石蕊试验,因为它能告诉我们一个函数是否是接受输入返回输出的小函数。比如,while 循环是不能组合的。不过你也要警惕,pointfree 就像是一把双刃剑,有时候也能混淆视听。并非所有的函数式代码都是 pointfree 的,不过这没关系。可以使用它的时候就使用,不能使用的时候就用普通函数。
// 非 pointfree,因为提到了数据:name
var initials = function(name) {
return name
.split(' ')
.map(compose(toUpperCase, head))
.join('. ')
}
// pointfree
var initials = compose(join('. '), map(compose(toUpperCase, head)), split(' '))
initials('hunter stockton thompson')
// 'H. S. T'
2
3
4
5
6
7
8
9
10
11
12
13
- debug 组合:组合的一个常见错误是,在没有局部调用之前,就组合类似 map 这样接受两个参数的函数。
// 错误做法:我们传给了 `angry` 一个数组,根本不知道最后传给 `map` 的是什么东西。
var latin = compose(map, angry, reverse)
latin(['frog', 'eyes'])
// error
// 正确做法:每个函数都接受一个实际参数。
var latin = compose(map(angry), reverse)
latin(['frog', 'eyes'])
// ["EYES!", "FROG!"])
2
3
4
5
6
7
8
9
10
11
- 同步通用组合函数。
const compose = (...fns) => result => {
const list = fns.slice()
while (list.length > 0) result = list.pop()(result)
return result
}
2
3
4
5
- 异步通用组合函数。
const composePromise = (...actions) => {
const init = actions.pop()
return (...args) =>
actions
.reverse()
.reduce(
(sequence, func) => sequence.then(result => func(result)),
Promise.resolve(init(...args))
)
}
2
3
4
5
6
7
8
9
10
# 3. Hindley-Milner 类型签名
- 类型签名在写纯函数时所起的作用非常大,大到英语都不能望其项背。这些签名轻轻诉说着函数最不可告人的秘密。短短一行,就能暴露函数的行为和目的。
// strLength :: String -> Number
var strLength = function(s) {
return s.length
}
// join :: String -> [String] -> String
var join = curry(function(what, xs) {
return xs.join(what)
})
// match :: Regex -> String -> [String]
var match = curry(function(reg, s) {
return s.match(reg)
})
// replace :: Regex -> String -> String -> String
var replace = curry(function(reg, sub, s) {
return s.replace(reg, sub)
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- map 接受两个参数,第一个是从任意类型 a 到任意类型 b 的函数。第二个是一个数组,元素是任意类型的 a。map 最后返回的是一个类型 b 的数组。
// map :: (a -> b) -> [a] -> [b]
var map = curry(function(f, xs) {
return xs.map(f)
})
2
3
4