阅读函数式编程(二)

在上一篇文章中,我们了解了关于纯函数的概念, 纯函数的好处体现了函数式编程的一大优点: 没有副作用, 那么什么是函数式编程, 与命令式编程有什么不同, 函数式编程有什么优点,这篇文章想要深入了解下函数式编程的基本概念以及特点;

基本概念

Wikipedia 上面, 这样解释函数式编程:
  • In computer science, functional programming is a programming paradigm or pattern (a style of building the structure and elements of computer programs)
  • Functional Programming treats computation as the evaluation of mathematical functions.
  • Functional Programming avoids changing-state and mutable data.
  • 在计算机中, 函数式编程是是一种编程范例或者模式(一种构建计算机程序的结构和元素的类型)。
  • 函数式编程将计算视作数学上函数的评估。
  • 函数编程中避免改变状态以及变化的数据。
函数式编程与命令式编程最大的区别在于:函数式编程关心数据的映射, 命令式编程关心解决问题的步骤。 这里的函数式编程之所以被称作函数,实际上类似于数学上函数的概念;
函数的定义:设A,B是非空的数集,如果按照某种确定的对应关系f,使对于集合A中的任意一个数x,在集合B中都有唯一确定的数 y 和 x 对应,那么变称映射 f: A ===> B 称为从集合 A 到 B 的一个映射
同样在函数式编程中, 函数式编程中的函数中, 每一个输入值都有一个唯一确定的输出值和输出值对应, 但是不同的输入值可以得到相同的输出值, 这种关系是一种映射关系。当然,对于函数编程而言,它不仅仅可以实现数据的映射, 还可以实现函数之间的映射。例如:如果我们想要对于数组中的每一个数加1:
1
2
3
4
5
const arr = [1, 2, 3];
for (let index in arr) {
arr[index] += 1;
}
console.log("arr", arr); // [2, 3, 4]
或者使用 forEach, map 中数组中定义的方法:
1
2
3
4
// forEach
arr.forEach(item => item += 1);
// map
const newArr = arr.map(item => item += 1);

注意:使用 map 以及 forEach 的方法都是对于一个数组进行遍历,接收的参数也是相同的, 但是, 这两个方法之间还是存在着一些区别的,使用 map 不会改变原数组,但是会返回经过函数运算之后的新数组。对于使用 forEach 而言, 使用 forEach 不会改变原来的数组,返回值为 undefined。从函数式编程的角度而言,使用 map 更能体现函数式编程的特点: 不会产生副作用;
在上面实现数组中每项加一的操作中, 使用两种代码实现的, 第一个代码中我们将想要计算机运行的步骤通过命令的方式写了下来,告诉计算机, 通过 for 循环循环这个数组,然后数组中的每一项进行加一操作, 而在第二种代码中, 我们通过类似于函数式编程的方式实现,我们不用关心这个过程是怎么实现的, 我们只要将这个数据转换的关系告诉函数就可以了,换句话说, 我们这里关心的是数据之间的映射。

几大特性

对于函数式编程而言,存在下面几种特性:
  • 高阶函数
  • 没有副作用
  • 函数柯里化
  • 闭包

高阶函数

高阶函数是这样一种函数: 函数被作为参数传入或者被作为返回值被返回的一类函数被称作高阶函数;例如下面一段 polifill es6promise 的代码中, 对于promise 返回成功状态或者失败状态的调用函数中是下面这样调用的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Promise (executor) {
let that = this;
that.status = PENDDING;
that.value = void 0;
that.handlerQueue = [];
// 执行函数, 传递进入 value
// executor(成功函数, 失败函数);
executor(function (value) {
// 成功函数执行,传递进入 transition 状态: FULFILLED
that.transition(FULFILLED, value);
}, function (value) {
// 失败函数执行, 传递进入 transition 状态: REJECTED
that.transition(REJECTED, value);
})
}
在上面的代码中, 这个 executor 函数是被作为参数传入到 Promise 函数中的, 同时这个 executor 也是接受两个函数作为参数, 一个是作为函数返回值为成功状态的函数, 另一个是作为函数返回值为失败状态的函数。例如计算下面代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function getSum(a, b, sum = 0) {
if (a < b - 1) {
return getSum(a + 1, b, sum + a + 1);
} else {
return sum;
}
}
function getSquare(a, b, sum = 0) {
if (a < b - 1) {
return getSquare(a + 1, b, sum + Math.pow(a + 1, 2));
} else {
return sum;
}
}
const sums = getSum(1, 4);
const squareSums = getSquare(1, 4);
上面两段代码分别是求两个数之间的整数和以及平方和;(不包括这两个数)实际上, 上面的两种方法都是进行函数求和的运算, 只是求和的过程是不一样的,上面的代码我们可以重写如下:
1
2
3
4
5
6
7
8
9
function sumFn(a, b, cb, sum = 0) {
if (a < b -1) {
return sumFn(a + 1, b, cb, cb(sum, a));
} else {
return sum;
}
}
const addSum = sumFn(1, 4, (sum, a) => sum + 1 + a);
const squareSum = sumFn(1, 4, (sum, a) => sum + Math.pow(a + 1, 2));
在上面的代码中, 我们抽取出了求和的函数, 通过向求和函数中的参数 cb 中传入一个函数进行求取。我们可以看到, 在 sumFn 这个函数中, 函数的逻辑取决于传入的参数cb的逻辑, 通过传入函数为参数的这种形式, 将程序的粒度控制在函数的层面上面。