JS `Category Theory` `Monads`, `Applicatives`, `Functors` 在复杂异步流中的应用

admin
Sun, 20 Jul 2025 10:07:20 +0000
Javascript, category, js, theory

各位靓仔靓女，晚上好！今天咱来聊聊编程界的“老干部”——范畴论，以及它在 JavaScript 异步世界里搞事情的那些事儿。别害怕，虽然名字听起来像哲学，但其实它能让你的代码更优雅、更可维护，还能让你在面试的时候显得特别有逼格。 今天咱们主要讲讲范畴论中的几个重要概念：Functor（函子）、Applicative（适用函子）和 Monad（单子），以及它们如何在复杂的 JavaScript 异步流程中大显身手。 第一部分：范畴论？那是啥玩意儿？ 别急着关掉网页，我保证不讲让你头疼的数学公式。咱们用更接地气的方式来理解。 范畴（Category）： 想象一下，你有一堆东西（对象），比如数字、字符串、函数等等。然后，你有一些操作（态射），可以把这些东西变成另外一些东西。范畴就是把这些对象和操作组织在一起的一个结构。 函子（Functor）： 函子就像一个容器，它可以包裹住你的数据，并且提供一个 map 方法，让你可以在不打开容器的情况下，对容器里面的数据进行操作。 适用函子（Applicative）： 适用函子比函子更强大，它可以让你把一个包裹在容器里的函数，应用到另一个包裹在容器里的数 …

继续阅读“JS `Category Theory` `Monads`, `Applicatives`, `Functors` 在复杂异步流中的应用”

The post JS `Category Theory` `Monads`, `Applicatives`, `Functors` 在复杂异步流中的应用 first appeared on 智猿学院-前后端，数据库，人工智能，云计算等领域前沿技术讲座.

各位靓仔靓女，晚上好！今天咱来聊聊编程界的“老干部”——范畴论，以及它在 JavaScript 异步世界里搞事情的那些事儿。别害怕，虽然名字听起来像哲学，但其实它能让你的代码更优雅、更可维护，还能让你在面试的时候显得特别有逼格。

今天咱们主要讲讲范畴论中的几个重要概念：Functor（函子）、Applicative（适用函子）和 Monad（单子），以及它们如何在复杂的 JavaScript 异步流程中大显身手。

第一部分：范畴论？那是啥玩意儿？

别急着关掉网页，我保证不讲让你头疼的数学公式。咱们用更接地气的方式来理解。

范畴（Category）： 想象一下，你有一堆东西（对象），比如数字、字符串、函数等等。然后，你有一些操作（态射），可以把这些东西变成另外一些东西。范畴就是把这些对象和操作组织在一起的一个结构。

函子（Functor）： 函子就像一个容器，它可以包裹住你的数据，并且提供一个 map 方法，让你可以在不打开容器的情况下，对容器里面的数据进行操作。

适用函子（Applicative）： 适用函子比函子更强大，它可以让你把一个包裹在容器里的函数，应用到另一个包裹在容器里的数据上。

单子（Monad）： 单子是适用函子的升级版，它可以让你把多个容器化的操作链接起来，形成一个管道，而且还能处理副作用。

是不是感觉有点抽象？没关系，咱们用代码来解释。

第二部分：Functor（函子）—— 让你的数据飞起来

函子的核心概念是 map。map 方法接受一个函数作为参数，然后把这个函数应用到容器里面的数据上，最后返回一个新的容器，里面装着处理后的数据。

举个例子，假设我们有一个 Maybe 函子，它可以处理空值的情况：

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

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

  map(fn) {
    return this._value == null ? Maybe.of(null) : Maybe.of(fn(this._value));
  }
}

// 使用示例
const safeDivide = (x, y) => {
  if (y === 0) {
    return null; // 避免除以零的错误
  }
  return x / y;
};

const result = Maybe.of(10)
  .map(x => x * 2)
  .map(x => safeDivide(x, 5))
  .map(x => x + 3);

console.log(result); // Maybe { _value: 7 }

const result2 = Maybe.of(10)
  .map(x => x * 2)
  .map(x => safeDivide(x, 0)) // 出现 null
  .map(x => x + 3);

console.log(result2); // Maybe { _value: null }

在这个例子中，Maybe.of(10) 创建了一个包裹着数字 10 的 Maybe 函子。然后，我们通过 map 方法，对这个数字进行了一系列操作。如果任何一个操作返回了 null，Maybe 函子就会自动返回 Maybe.of(null)，避免了空指针错误。

第三部分：Applicative（适用函子）—— 函数也装进容器

适用函子的核心概念是 ap。ap 方法接受一个包裹在容器里的函数作为参数，然后把这个函数应用到另一个包裹在容器里的数据上，最后返回一个新的容器，里面装着处理后的数据。

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

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

  map(fn) {
    return this._value == null ? Maybe.of(null) : Maybe.of(fn(this._value));
  }

  ap(other) {
    return this._value == null ? Maybe.of(null) : other.map(this._value);
  }
}

// 使用示例
const add = x => y => x + y;

const maybeAdd = Maybe.of(add); // Maybe { _value: [Function: add] }
const maybeTwo = Maybe.of(2);    // Maybe { _value: 2 }

const result = maybeAdd.ap(maybeTwo).map(x => x * 3); //(2+undefined)*3 Nan

console.log(result); // Maybe { _value: NaN }

在这个例子中，Maybe.of(add) 创建了一个包裹着 add 函数的 Maybe 函子。然后，我们通过 ap 方法，把这个函数应用到了另一个包裹着数字 2 的 Maybe 函子上。最后，我们通过 map 方法，对结果进行了进一步的处理。

第四部分：Monad（单子）—— 异步世界的救星

单子的核心概念是 flatMap（或者 chain、bind）。flatMap 方法接受一个函数作为参数，这个函数接受容器里面的数据作为参数，然后返回一个新的容器。flatMap 会自动把这个新的容器“拍平”，返回一个单一的容器。

单子最擅长处理的就是异步操作。想象一下，你需要依次执行多个异步操作，而且每个操作的结果都依赖于前一个操作的结果。如果使用传统的 Promise，你需要写很多嵌套的 then 方法，代码会变得非常难以阅读和维护。

但是，如果使用单子，你可以把这些异步操作链接起来，形成一个管道，代码会变得非常简洁和优雅。

class IO {
  constructor(fn) {
    this._fn = fn;
  }

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

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

  flatMap(fn) {
    return new IO(() => fn(this._fn())._fn());
  }

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

// 模拟异步操作
const readFile = filename => {
  return new IO(() => {
    console.log('Reading file:', filename);
    return `Contents of ${filename}`; // 假设读取文件成功
  });
};

const writeFile = (filename, content) => {
  return new IO(() => {
    console.log('Writing to file:', filename);
    return `Wrote "${content}" to ${filename}`; // 假设写入文件成功
  });
};

// 使用示例
const program = readFile('input.txt')
  .map(content => content.toUpperCase())
  .flatMap(upperCaseContent => writeFile('output.txt', upperCaseContent));

// 运行程序
const result = program.run();
console.log(result);

在这个例子中，readFile 和 writeFile 函数都返回 IO 单子。我们通过 flatMap 方法，把这两个操作链接起来，形成了一个管道。program.run() 方法会依次执行这两个操作，并返回最终的结果。

第五部分：Monad 在复杂异步流中的应用场景

1. 错误处理： 就像 Maybe 函子一样，我们可以创建一个 Either 单子，它可以处理错误的情况。Either 单子有两个子类：Left 和 Right。Right 表示成功，Left 表示失败。当任何一个操作返回 Left 时，整个管道就会停止执行，并返回 Left。

class Either {
  constructor(value) {
    this._value = value;
  }

  static of(value) {
    return new Right(value);
  }

  map(fn) {
    return this instanceof Left ? this : Either.of(fn(this._value));
  }

  flatMap(fn) {
    return this instanceof Left ? this : fn(this._value);
  }
}

class Left extends Either {}

class Right extends Either {}

const safeDivide = (x, y) => {
  if (y === 0) {
    return new Left('Division by zero');
  }
  return new Right(x / y);
};

const result = Either.of(10)
  .flatMap(x => safeDivide(x, 2))
  .flatMap(x => safeDivide(x, 0)) // 出现错误
  .map(x => x + 3);

console.log(result); // Left { _value: 'Division by zero' }

2. 状态管理： 我们可以创建一个 State 单子，它可以管理状态。State 单子接受一个函数作为参数，这个函数接受当前状态作为参数，然后返回一个新的状态和一个结果。

class State {
  constructor(fn) {
    this._fn = fn;
  }

  static of(value) {
    return new State(state => [value, state]);
  }

  map(fn) {
    return new State(state => {
      const [value, newState] = this._fn(state);
      return [fn(value), newState];
    });
  }

  flatMap(fn) {
    return new State(state => {
      const [value, newState] = this._fn(state);
      return fn(value)._fn(newState);
    });
  }

  run(initialState) {
    return this._fn(initialState);
  }
}

// 示例：计数器
const increment = () => {
  return new State(state => [state + 1, state + 1]);
};

const program = increment()
  .flatMap(() => increment())
  .flatMap(() => increment());

const [result, finalState] = program.run(0);

console.log('Result:', result);        // Result: 3
console.log('Final State:', finalState); // Final State: 3

3. 异步流程控制： 结合 Promise 和 IO 单子，我们可以创建更加复杂的异步流程控制。

const IO = require('io-ts/lib/IO').IO;

const readFilePromise = filename => {
    return new Promise((resolve, reject) => {
        setTimeout(() => { // 模拟异步读取文件
            const content = `Contents of ${filename}`;
            console.log(`Read file: ${filename}`);
            resolve(content);
        }, 500);
    });
};

const writeFilePromise = (filename, content) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => { // 模拟异步写入文件
            console.log(`Wrote to file: ${filename}`);
            resolve(`Wrote "${content}" to ${filename}`);
        }, 500);
    });
};

const readFileIO = filename => {
    return new IO(() => readFilePromise(filename));
};

const writeFileIO = (filename, content) => {
    return new IO(() => writeFilePromise(filename, content));
};

const program = readFileIO('input.txt')
    .map(promise => promise.then(content => content.toUpperCase()))
    .flatMap(upperCaseContentPromise => writeFileIO('output.txt', upperCaseContentPromise.then(upperCaseContent => upperCaseContent)));

program.run().then(finalResultPromise => {
    finalResultPromise.then(finalResult => {
      console.log("Final Result:", finalResult);
    })
});

第六部分：总结

概念 | 作用 | 核心方法 | 适用场景
Functor | 包裹数据，提供 map 方法，对容器里的数据进行操作。 | map | 对容器里的数据进行转换，避免空指针错误。
Applicative | 包裹函数，提供 ap 方法，把容器里的函数应用到容器里的数据上。 | ap | 把多个容器里的数据组合起来，生成一个新的容器。
Monad | 包裹数据，提供 flatMap 方法，把多个容器化的操作链接起来，形成一个管道，处理副作用。 | flatMap | 处理异步操作、错误处理、状态管理等复杂场景。

范畴论的概念可能有点抽象，但是它们在 JavaScript 异步编程中非常有用。通过使用 Functor、Applicative 和 Monad，你可以写出更加简洁、优雅、可维护的代码。

当然，范畴论还有很多其他的概念，比如 Traversal、Lens 等等。如果你对这些概念感兴趣，可以自己去深入研究。

最后，送给大家一句话：编程的最高境界，就是把复杂的问题简单化。而范畴论，就是帮助你达到这个境界的工具之一。

今天就到这里，谢谢大家！