函数式编程范式

函数式编程范式

注:本文部分代码涉及到一些 JavaScript 新特性,需要提前了解一下: JavaScript 新特性

讲解视频: https://www.bilibili.com/video/BV1vV411a7kH/

1 函数式编程范式

1.1 定义

函数式编程(Functional Programming: FP)是一种编程范式(指计算机中编程中的典范模式或方法,就是一种思维方式),属于结构化编程,用于描述数据(函数)之间的映射关系。

特别需要注意的是,函数式编程中的函数不是指程序中的函数(方法),而是数学中的函数(映射关系),如: y = f(x) ,指 x y 之间的关系。

常见的编程范式有:过程化(命令式)编程、面向对象编程、声明式编程等。

过程化编程:最原始的传统编程,将问题抽象为一系列步骤,然后通过编程方式将这些步骤转换为程序指令集,这些指令集按照一定顺序排列。人们把支持过程化编程范式的编程语言称为过程化编程语言,常见的有机器语言、汇编语言、BASIC、C、FORTRAN 等。过程化语言特别适合解决线性(或者说按部就班)的算法问题。

面向对象编程:将待解决问题抽象为面向对象的程序中的对象,利用封装使每个对象都拥有个体的身份。程序就是成堆的对象,彼此通过信息的传递,请求其它对象进行工作。面向对象包括三个基本概念:封装性、继承性、多态性。常见的面向对象语言有 Java、C、C++、JavaScript。

声明式编程:以数据结构的形式来表达程序执行的逻辑。它的主要思想是告诉计算机应该做什么,但不指定具体要怎么做。SQL 语句就是最明显的一种声明式编程的例子,我们只需要定义好该如何处理数据,不需要指定具体实现,就可以查询到我们需要的数据。

现代编程语言的发展趋势是支持多种范式,如 C#、Java 8+、Kotlin、ES6+。

编程范式和设计模式的区别

  • 编程范式:是指从事软件工程的一类典型的编程风格(此概念好比“战略”),体现编写程序的人如何看待程序设计的“哲学观”;
  • 程序设计模式:设计模式是软件设计中常见问题的典型解决方案(此概念好比“战术”),是解决一系列实际问题的“方法学”。
  • 1.2 特点

    1. 代码简洁:函数式编程使用了大量的函数,减少了代码的重复;

    2. 接近自然语言,易于理解:

      1
      2
      3
      let result = (1 + 2)* 3 - 4; // 普通表达式

      let result = subtract(multiply(add(1, 2), 3), 4); // 函数式编程
    3. 函数是“第一等公民”:函数与其他数据类型一样,处于平等地位,可以赋值给其它变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值;

    4. 闭包和高阶函数:函数式编程会使用较多的闭包和高阶函数;

    5. 没有“副作用”,方便与代码管理和单元测试: 副作用 指函数内部与外部互动(最典型的情况,就是修改全局变量量的 值),产⽣运算以外的其他结果。函数式编程强调没有”副作用”,意味着函数要保持独立,所有功能就是返回一个新的值,没有其他⾏为,尤其是不得修改外部变量的值;

    6. 引用透明:函数的运行不依赖于外部变量或”状态”,只依赖于输入的参数,任何时候只要参数相同,引用函数所得到的返回值总是相同的。

      1.3 基本概念

      ① 函数式编程的思维方式:

      把现实世界的事物和事物之间的联系(映射关系)抽象到程序世界(对运算过程进行抽象)

      1
      2
      // 比如买单价为0.5元的白菜,买了两斤,需要支付多少块钱(白菜与货币的联系)
      let money = multiply(0.5, 2); // 即两斤白菜 -> 1元(money)

      ② 程序的本质:

      根据输入通过某种运算获得相应的输出,程序开发过程中会涉及很多有输入和输出的函数。

      ③ 函数 y = f(x)

      x → f(映射) → y

      图 1 从 x 到 y 的函数关系

      图 2 从 x 到 y 不是函数关系

      ④ 纯函数:相同的输入始终要得到相同的输出

      ⑤ 函数式编程是用来描述数据(函数)之间的映射

      1.4 学习指南

      函数式编程范式只是一种对程序编程思维的一种概论,而具体的实现则通过柯里化(第 5 章)、函数组合(第 6 章)、函子等来实现。

      在学习如何实现前,需要先了解三个小知识点:头等函数(第 2 章),闭包(第 3 章),纯函数(第 4 章)。

      2 头等函数

      2.1 函数是一等公民

      函数是一等公民?通俗来讲在某些编程语言中,函数是不能够:

    7. 函数可以存储在变量中
    8. 函数可以作为参数(2.2.1)
    9. 函数可以作为返回值(2.2.2)
    10. JavaScript 对待不同的数据结构具有同等级别的支持,函数可以享受以上几种待遇,所以在 JavaScript 中,函数是一等公民。

      函数可以存储在变量中:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      // 把函数赋值给变量
      const fn = () => {
      console.log("2.1 函数可以存储在变量中");
      };
      fn();

      // 属性赋值示例
      const objController = {
      getKeys(obj) {
      // ES6属性简写,等同于getKeys: getKeys(obj)
      return Object.keys(obj);
      },
      };
      // 优化:上面代码中getKeys方法和内部调用Object.keys方法的参数和返回值一样,所以可以改写成下面
      const objController = {
      getKeys: Object.keys,
      };
      console.log(objController);

      const my = { name: "Patrick Jun", constellation: "Virgo" };
      console.log(objController.getKeys(my));

      2.2 高阶函数

      如果一个函数以下面任一方式使用,那么这个函数就可以称为高阶函数。

    11. 参数是一个函数
    12. 返回值是一个函数
    13. Patrick Jun:可以操作函数的函数就是高阶函数。这就跟高数里的求导(二阶及以上的求导称之为高阶求导)一样,可以对已导函数的求导就是高阶求导。

      2.2.1 函数作为参数

      将函数作为参数最常见的就是咱们经常用的回调函数。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      // 遍历(模拟数组的forEach方法)
      function forEach(arr, fn) {
      for (let i = 0; i < arr.length; i++) {
      fn(arr[i], i); // 将每一项传入回调fn处理
      }
      }

      // 筛选,返回符合条件的元素组成的新数组
      function filter(arr, fn) {
      const results = [];
      for (const item of arr) {
      if (fn(item)) {
      results.push(item);
      }
      }
      return results;
      }

      const colors = ["#FF0000", "#00FF00", "blue"];
      forEach(colors, (item, index) => {
      console.log(index + 1, item);
      });
      console.log(filter(colors, (item) => item.length === 7));

      2.2.2 函数作为返回值

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      function makeFn() {
      const msg = "hello function";
      return function () {
      console.log(msg);
      };
      }
      const fn = makeFn(); // makeFn()执行后返回一个匿名函数,赋值给fn
      fn();
      // makeFn()()

      // 模拟lodash中的once函数 对一个函数只执行一次的函数(例如支付,不管用户点击多少次按钮,都只执行一次)
      function once(func) {
      let done = false; // 定义一个状态done,判断是否已执行支付
      return function () {
      if (!done) {
      done = true; // 更改闭包作用域中的done为已支付
      func.apply(this, arguments);
      }
      };
      }
      const pay = once((money) => {
      // 传入一个函数,通过输出模拟支付过程和结果
      console.log(`支付${money}元`);
      });
      pay(20); // 支付20元
      pay(30);
      pay(40);

      2.2.3 使用高阶函数意义