JS `Category Theory` 概念在函数式编程中的应用 (`Functor`, `Monad`)

admin
Thu, 17 Jul 2025 10:01:40 +0000
Javascript, category, js, theory

各位观众老爷，大家好！今天咱们不聊明星八卦，不聊房价涨跌，咱们来聊点硬核的——JavaScript 中的范畴论，特别是 Functor 和 Monad 这两个听起来高大上，实际上理解起来也…有点绕的概念。 别怕，咱们争取用最接地气的方式，把它们扒个精光！ 范畴论是个啥玩意儿？ 在开始 Functor 和 Monad 之前，我们需要简单了解一下什么是范畴论。 别担心，我们不会深入到数学的海洋，只会浅尝辄止。 范畴论是一种抽象的数学理论，它研究的是对象和对象之间的关系（称为态射）。 你可以把范畴想象成一个由点和箭头组成的网络： 对象 (Objects): 可以是任何东西，比如数字、类型、函数、甚至整个程序！ 态射 (Morphisms): 就是对象之间的箭头，表示对象之间的关系。在编程中，通常是函数。 范畴论最重要的概念是组合 (Composition)。 如果有一个从 A 到 B 的态射 f，和一个从 B 到 C 的态射 g，那么就可以将它们组合成一个从 A 到 C 的态射 g f (读作 "g after f" 或者 "g composed with …

继续阅读“JS `Category Theory` 概念在函数式编程中的应用 (`Functor`, `Monad`)”

The post JS `Category Theory` 概念在函数式编程中的应用 (`Functor`, `Monad`) first appeared on 智猿学院-前后端，数据库，人工智能，云计算等领域前沿技术讲座.

各位观众老爷，大家好！今天咱们不聊明星八卦，不聊房价涨跌，咱们来聊点硬核的——JavaScript 中的范畴论，特别是 Functor 和 Monad 这两个听起来高大上，实际上理解起来也…有点绕的概念。 别怕，咱们争取用最接地气的方式，把它们扒个精光！

范畴论是个啥玩意儿？

在开始 Functor 和 Monad 之前，我们需要简单了解一下什么是范畴论。 别担心，我们不会深入到数学的海洋，只会浅尝辄止。

范畴论是一种抽象的数学理论，它研究的是对象和对象之间的关系（称为态射）。 你可以把范畴想象成一个由点和箭头组成的网络：

对象 (Objects): 可以是任何东西，比如数字、类型、函数、甚至整个程序！

态射 (Morphisms): 就是对象之间的箭头，表示对象之间的关系。在编程中，通常是函数。

范畴论最重要的概念是组合 (Composition)。 如果有一个从 A 到 B 的态射 f，和一个从 B 到 C 的态射 g，那么就可以将它们组合成一个从 A 到 C 的态射 g f (读作 "g after f" 或者 "g composed with f")。

组合需要满足结合律：(h g) f = h (g f)。 这保证了组合的顺序不影响最终结果。

还有一个重要的概念是单位态射 (Identity Morphism)。 对于每个对象 A，都存在一个从 A 到 A 的单位态射 idA，它满足 f idA = f 和 idB f = f，其中 f 是一个从 A 到 B 的态射。

范畴论在编程中的意义

你可能会问，这跟编程有什么关系？ 范畴论提供了一种思考和组织代码的新方式，它强调组合性和抽象性。 通过将代码视为对象和态射，我们可以利用范畴论的工具来设计更模块化、更可复用、更易于维护的代码。

主角登场：Functor (函子)

好了，铺垫完毕，现在让我们请出今天的第一位主角：Functor！

Functor 是什么？

简单来说，Functor 是一个容器，它能让你对容器里面的东西进行操作，而不用关心容器本身的结构。 更正式地说，Functor 是一个类型构造器 (type constructor) 和一个函数 fmap (或者 map)。

类型构造器: 接受一个类型 A，返回一个新的类型 F。 比如，数组就是一个类型构造器。 Array 就是一个 Functor， 它接受类型 number，返回一个新的类型：数字类型的数组。

fmap (或者 map): 接受一个函数 f: A -> B 和一个 Functor F，返回一个新的 Functor F。 也就是说，fmap 可以将一个函数应用到 Functor 里面的每一个元素，并返回一个新的 Functor，包含了应用函数后的结果。

Functor 的定律

一个合法的 Functor 必须满足两个定律：

1. Identity Law (单位律): fmap(x => x, functor) === functor。 将单位函数 (返回自身) 应用到 Functor 上，应该返回原始 Functor。

2. Composition Law (组合律): fmap(f, fmap(g, functor)) === fmap(x => f(g(x)), functor)。 先应用 g，再应用 f，和直接应用 f(g(x)) 应该得到相同的结果。

JavaScript 中的 Functor 例子

1. Array Functor:

数组是 JavaScript 中最常见的 Functor 之一。 map 方法就是 fmap 的实现。

const numbers = [1, 2, 3, 4, 5];

// 使用 map (fmap) 将每个数字乘以 2
const doubledNumbers = numbers.map(x => x * 2);

console.log(doubledNumbers); // [2, 4, 6, 8, 10]

我们可以验证一下 Functor 的定律：

const numbers = [1, 2, 3];

// Identity Law
const identity = x => x;
console.log(numbers.map(identity).every((v, i) => v === numbers[i])); // true

// Composition Law
const f = x => x * 2;
const g = x => x + 1;
const composed = x => f(g(x));

console.log(numbers.map(g).map(f).every((v, i) => v === numbers.map(composed)[i])); // true

2. Maybe/Optional Functor:

Maybe/Optional Functor 用于处理可能为空的值。 它有两种状态：Just (包含一个值) 和 Nothing (表示空)。

class Maybe {
  constructor(value) {
    this.value = value;
  }

  static Just(value) {
    return new Maybe(value);
  }

  static Nothing() {
    return new Maybe(null); // or undefined
  }

  map(fn) {
    if (this.value === null || this.value === undefined) {
      return Maybe.Nothing();
    } else {
      return Maybe.Just(fn(this.value));
    }
  }
}

// 使用 Maybe Functor
const safeDivide = (x, y) => {
  if (y === 0) {
    return Maybe.Nothing();
  } else {
    return Maybe.Just(x / y);
  }
};

const result1 = safeDivide(10, 2).map(x => x * 3); // Maybe.Just(15)
const result2 = safeDivide(10, 0).map(x => x * 3); // Maybe.Nothing()

console.log(result1);
console.log(result2);

Maybe Functor 的好处在于，它可以避免空指针异常，并且可以链式调用 map 方法，而不用担心中间结果为空。

Functor 的作用

抽象: Functor 隐藏了容器的具体实现，允许你以一种通用的方式操作容器里面的值。

组合: Functor 允许你将多个操作组合在一起，形成一个流水线，提高代码的可读性和可维护性。

避免副作用: Functor 鼓励使用纯函数，从而减少副作用，提高代码的可靠性。

重量级嘉宾：Monad (单子)

接下来，让我们隆重请出今天的压轴大戏：Monad！

Monad 是什么？

Monad 就像一个更强大的 Functor。 它也提供了一种容器，但它不仅仅能让你对容器里面的值进行操作，还能处理嵌套的容器。

更正式地说，Monad 是一个类型构造器和一个函数 bind (或者 flatMap)。

类型构造器: 和 Functor 一样，接受一个类型 A，返回一个新的类型 M。

bind (或者 flatMap): 接受一个函数 f: A -> M 和一个 Monad M，返回一个新的 Monad M。 注意，f 返回的是一个 Monad，而不是一个普通的值。 bind 的作用是将 f 应用到 Monad 里面的值，并将结果“扁平化”，从而避免嵌套的 Monad。

return (或者 of, unit): 接受一个类型 A, 返回一个新的 Monad M. 它将一个普通值放入 Monad 容器中。

Monad 的定律

一个合法的 Monad 必须满足三个定律：

1. Left Identity Law (左单位律): return(x).bind(f) === f(x)。 将一个值放入 Monad，然后用一个函数处理它，应该和直接将函数应用于该值得到相同的结果。

2. Right Identity Law (右单位律): monad.bind(return) === monad。 将 Monad 绑定到 return 函数，应该返回原始 Monad。

3. Associativity Law (结合律): monad.bind(f).bind(g) === monad.bind(x => f(x).bind(g))。 先绑定 f，再绑定 g，和先将 f 和 g 组合成一个函数，再绑定到 Monad 应该得到相同的结果。

JavaScript 中的 Monad 例子

1. Maybe/Optional Monad: (和上面的 Maybe Functor 类似，但增加了 bind 方法)

class Maybe {
  constructor(value) {
    this.value = value;
  }

  static Just(value) {
    return new Maybe(value);
  }

  static Nothing() {
    return new Maybe(null); // or undefined
  }

  map(fn) {
    if (this.value === null || this.value === undefined) {
      return Maybe.Nothing();
    } else {
      return Maybe.Just(fn(this.value));
    }
  }

  bind(fn) { // flatMap
    if (this.value === null || this.value === undefined) {
      return Maybe.Nothing();
    } else {
      return fn(this.value);
    }
  }

  static return(value) { // of
    return Maybe.Just(value);
  }
}

// 使用 Maybe Monad
const safeDivide = (x, y) => {
  if (y === 0) {
    return Maybe.Nothing();
  } else {
    return Maybe.Just(x / y);
  }
};

const result1 = Maybe.Just(10).bind(x => safeDivide(x, 2)).bind(x => Maybe.Just(x * 3)); // Maybe.Just(15)
const result2 = Maybe.Just(10).bind(x => safeDivide(x, 0)).bind(x => Maybe.Just(x * 3)); // Maybe.Nothing()

console.log(result1);
console.log(result2);

注意 bind 的使用，它可以处理 safeDivide 返回的 Maybe 类型，避免了嵌套的 Maybe。

2. IO Monad:

IO Monad 用于处理副作用 (比如输入/输出、网络请求等)。 它将副作用延迟到程序的最外层执行，从而保证程序的纯洁性。

class IO {
  constructor(effect) {
    this.effect = effect;
  }

  static of(value) {
    return new IO(() => value);
  }

  map(fn) {
    return new IO(() => fn(this.effect()));
  }

  bind(fn) { // flatMap
    return new IO(() => fn(this.effect()).effect());
  }

  run() {
    return this.effect();
  }
}

// 使用 IO Monad
const readLine = () => new IO(() => prompt("Enter your name:")); // 模拟读取用户输入
const printLine = (str) => new IO(() => console.log(str)); // 模拟打印输出

const program = readLine().bind(name => printLine(`Hello, ${name}!`));

// 运行程序
program.run();

IO Monad 的关键在于它将副作用封装在一个函数 effect 中，只有在调用 run 方法时才会执行。 这样可以保证程序的其他部分都是纯的。

3. Promise Monad:

Promise 本身就是一个 Monad! then 方法就是 bind 的实现。

const fetchUserData = (userId) =>
  Promise.resolve(userId)
    .then(id => `User data for ID: ${id}`)

const logUserData = (userData) =>
  Promise.resolve(userData)
    .then(data => console.log(data))

fetchUserData(123).then(logUserData)

Monad 的作用

处理副作用: Monad 可以将副作用隔离在程序的最外层，从而保证程序的纯洁性。

控制执行顺序: Monad 可以控制程序的执行顺序，例如，可以使用 Maybe Monad 来处理可能为空的值，可以使用 IO Monad 来处理异步操作。

组合复杂的操作: Monad 可以将多个操作组合在一起，形成一个复杂的流程，提高代码的可读性和可维护性。

避免嵌套回调: Monad 可以有效避免嵌套回调(callback hell)，使代码更易于理解和维护。

Functor vs. Monad: 区别在哪？

特性 | Functor | Monad
主要作用 | 对容器内的值进行操作 | 处理嵌套的容器，处理副作用
关键函数 | map (或者 fmap) | bind (或者 flatMap), return (or of)
处理嵌套 | 不能处理嵌套的容器 | 可以处理嵌套的容器
是否处理副作用 | 一般不处理副作用 | 可以处理副作用

简单来说，所有的 Monad 都是 Functor，但不是所有的 Functor 都是 Monad。 Monad 比 Functor 更强大，它提供了更高级的抽象和控制能力。

总结

今天我们一起学习了范畴论的一些基本概念，以及 Functor 和 Monad 在 JavaScript 中的应用。 虽然这些概念一开始可能有点抽象，但只要你多练习、多思考，就能逐渐掌握它们，并用它们来编写更优雅、更健壮的代码。

范畴论不仅仅是一种理论，更是一种思考问题的方式。 它可以帮助我们更好地理解代码的本质，从而设计出更优秀的软件。

希望今天的讲座对你有所帮助！ 下次有机会，我们再聊聊其他的函数式编程概念。 下课！