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

各位观众老爷,大家好!今天咱们不聊明星八卦,不聊房价涨跌,咱们来聊点硬核的——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 = fidB ∘ f = f,其中 f 是一个从 A 到 B 的态射。

范畴论在编程中的意义

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

主角登场:Functor (函子)

好了,铺垫完毕,现在让我们请出今天的第一位主角:Functor!

Functor 是什么?

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

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)。

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 中的应用。 虽然这些概念一开始可能有点抽象,但只要你多练习、多思考,就能逐渐掌握它们,并用它们来编写更优雅、更健壮的代码。

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

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

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注