函数式编程

206 阅读8分钟

这篇笔记📒主要记录函数式编程,

  1. 什么是函数式编程?
  2. 函数式编程的优缺点
  3. 什么是纯函数?
  4. 什么是函数的副作用?
什么是函数式编程

函数式编程(Functional Programming, FP)是一种编程范式,强调使用纯函数、不可变数据结构和函数的组合来构建程序。它主要基于数学中的函数概念,旨在通过减少副作用、提高代码的可读性、可测试性和可维护性。

以下是函数式编程的一些核心概念和特点:

  1. 纯函数
    纯函数是指在相同的输入下总是返回相同的输出,并且没有副作用。它不依赖于外部状态,也不会修改外部状态。这使得纯函数非常容易测试和调试。
// 纯函数示例
function todo(weather) {
  return weather === '下雨' ? '不出门‘ : ’出去玩啊';
}
  1. 不可变性
    在函数式编程中,数据是不可变的。一旦创建,就不能修改。所有的数据变更都会产生新的数据结构,而不是在原有数据结构上进行修改。这可以避免意外的状态变化和数据竞争问题。
const list = [1, 2, 3];
const newList = [...list, 4]; // 原始 list 不变,newList 是一个新数组
  1. 高阶函数
    高阶函数是指可以接收函数作为参数或将函数作为返回值的函数。这使得函数可以像数据一样进行操作,支持函数的组合和柯里化。
function takeNotes(note) {
  return function(adjective) {
    return adjective + " " + note;
  }
}
const note = takeNotes("函数式编程");
note('JavaScript笔记:')
console.log(note('JavaScript笔记:'))
  1. 函数组合
    函数组合是将多个小函数组合成一个更复杂的函数,使得代码更加模块化和可复用。函数组合通常通过类似 compose 或 pipe 的函数来实现。
const compose = (f, g) => x => f(g(x));
const addOne = x => x + 1;
const square = x => x * x;

const addOneAndSquare = compose(square, addOne);
console.log(addOneAndSquare(2)); // 输出 9
  1. 引用透明
    引用透明是指函数的调用可以被其返回值所替代,而不会改变程序的行为。这是函数式编程中的一个重要属性,因为它意味着程序的行为可以被静态分析和优化。

  2. 递归
    由于函数式编程避免使用可变状态,递归通常被用来代替循环来处理数据结构。

  3. 延迟计算
    函数式编程支持惰性求值,即只有在需要时才计算表达式的值。这使得程序可以处理无限的数据结构(如无限列表)。

函数式编程优点
  • 可测试性强:纯函数易于测试。
  • 并行性好:由于没有副作用,纯函数可以轻松并行化。
  • 可维护性高:通过函数组合和不可变数据结构,代码更模块化和可预测。
函数式编程缺点
  • 性能开销:频繁创建新数据结构可能带来性能开销。
  • 学习曲线:对于习惯命令式编程的开发者,函数式编程可能有一定的学习曲线。
什么是纯函数

在编程中,纯函数(pure function)是指一个函数满足以下两个条件:

  • 相同的输入永远返回相同的输出:给定相同的输入,纯函数总是返回相同的结果,没有任何随机性或依赖外部状态。

  • 没有副作用:纯函数不依赖于外部的状态或改变外部的状态。它不会修改外部变量,也不会进行I/O操作(如读写文件、打印日志等)。

由于这些特性,纯函数非常容易测试和调试,也适合并行执行,因为它们不会产生竞态条件或副作用。

// 纯函数
function add(a, b) {
  return a + b;
}

// 非纯函数,因为它依赖于外部的状态
let x = 10;
function addToX(y) {
  return x + y;
}

add(a, b) 是一个纯函数,因为它的输出完全依赖于输入参数 a 和 b,且没有副作用。而 addToX(y) 是一个非纯函数,因为它依赖于外部变量 x,且可能会因为 x 的改变而产生不同的结果。

函数的副作用

函数的副作用(side effects)指的是函数在执行过程中除了返回值之外,还对外部环境产生了额外的影响。这些影响可能包括修改外部变量、修改全局状态、执行 I/O 操作(如读写文件、打印日志、发送网络请求等),或者调用其他有副作用的函数。

副作用的例子:

  1. 修改外部变量:
let count = 0;
function increment() {
  count++; // 修改了外部变量 count
}

在这个例子中,increment 函数改变了外部的变量 count,这是一个副作用。

  1. 修改全局状态:
let state = { loggedIn: false };
function login() {
  state.loggedIn = true; // 修改了全局状态 state
}

这里 login 函数改变了全局的 state 对象,产生了副作用。

  1. 执行 I/O 操作:
function logMessage(message) {
  console.log(message); // 执行了 I/O 操作(打印日志)
}

logMessage 函数执行了一个 I/O 操作,向控制台输出了信息,这是一个副作用。

  1. 调用有副作用的函数:
function updateUser(user) {
  saveToDatabase(user); // 假设 saveToDatabase 是一个有副作用的函数
}

如果 saveToDatabase 函数在调用时会修改数据库,那么 updateUser 函数也会被认为有副作用。

副作用使得函数的行为难以预测和测试,因为同样的输入可能由于外部环境的变化而导致不同的结果。因此,在某些编程范式(如函数式编程)中,尽量避免或最小化副作用,以保持代码的可预测性和可靠性。

Tip 并行性好:由于没有副作用,纯函数可以轻松并行化。 什么是并行性

并行性(Parallelism)是指在计算机科学中,同时执行多个计算任务以提高程序的效率和性能的能力。通过并行执行任务,可以显著减少总的执行时间。

并行性的基本概念
  • 多核处理器:现代计算机通常配备多个处理器内核(多核),每个内核可以独立地执行任务。并行性可以利用这些内核同时处理不同的任务。

  • 线程和进程:并行性通常通过多个线程或进程来实现,每个线程或进程可以独立地执行代码。线程共享同一进程的内存空间,而进程有自己独立的内存空间。

  • 任务划分:在并行计算中,任务通常被划分为多个子任务,这些子任务可以同时在不同的处理器或内核上执行。

并行性与纯函数

纯函数具有以下特点,使得它们在并行计算中特别有用

  1. 没有副作用:纯函数不依赖或改变外部状态,因此多个纯函数可以同时运行而不互相干扰。这减少了在并行执行时可能出现的竞态条件(race conditions)和死锁问题。

  2. 相同的输入总是产生相同的输出:因为纯函数是确定性的,不管在哪个线程或处理器上执行,结果都不会改变。这使得在分布式系统中,可以将任务分配到不同的节点执行。

并行性与并发的区别
  • 并行性:指同时执行多个任务(真正的同时执行),通常涉及多个处理器或内核。并行性旨在缩短任务的总执行时间。
  • 并发性(Concurrency):指在一段时间内交替处理多个任务,看起来像是同时进行,但实际上在某一时刻只有一个任务在执行。并发性更多关注任务的管理和调度。
并行性的例子
  1. 矩阵运算:在科学计算中,矩阵乘法可以通过将矩阵分割成子矩阵,并行计算每个子矩阵的结果来加速。

  2. 图像处理:处理高分辨率图像时,可以将图像分成多个块,同时对每个块进行滤波、增强或其他操作。

  3. 大规模数据处理:在分布式系统(如Hadoop、Spark)中,数据集可以被划分成多个部分,分发到不同的计算节点进行并行处理。

并行编程的挑战
  • 任务划分:合理地划分任务以均衡工作负载,避免某些处理器过载而其他处理器闲置。
  • 同步和通信:在并行执行的任务之间进行必要的数据同步和通信,避免数据不一致和竞态条件。
  • 死锁:由于资源竞争和锁机制的不当使用,可能会导致系统在等待资源时进入死锁状态。

通过利用并行性,可以大大提高程序的执行速度,特别是在处理大规模数据、复杂计算或实时系统时。