函数式编程指北

coderljw 2024-10-13 大约 5 分钟

# 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
}
1
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)
  }
})
1
2
3
4
5
  • 追求“纯”的理由。

    1. 可缓存性(利用闭包):纯函数总能够根据输入来做缓存。
    2. 可移植性/自文档化:纯函数是完全自给自足的,它需要的所有东西都能轻易获得。
    3. 可测试性:纯函数让测试更加容易。只需简单地给函数一个输入,然后断言输出就好了。
    4. 合理性:很多人相信使用纯函数最大的好处是引用透明性。如果一段代码可以替换成它执行所得的结果,而且是在不改变整个程序行为的前提下替换的,那么我们就说这段代码是引用透明的。

# 2. 代码组合

  • 这就是组合(compose)。f 和 g 都是函数,x 是在它们之间通过“管道”传输的值。
var compose = function(f, g) {
  return function(x) {
    return f(g(x))
  }
}
1
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'
1
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!"])
1
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
}
1
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))
      )
}
1
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)
})
1
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)
})
1
2
3
4
以父之名
周杰伦.mp3