JavaScript 中的函子(Functor)与单子(Monad):Maybe 与 Either Monad 的实战错误处理

JavaScript 中的函子(Functor)与单子(Monad):Maybe 与 Either Monad 的实战错误处理

大家好,欢迎来到今天的编程技术讲座。今天我们不讲“Hello World”,也不讲“闭包陷阱”,而是深入探讨一个在函数式编程中非常重要的概念——函子(Functor)和单子(Monad),并聚焦于两个最实用的类型:MaybeEither,它们能帮我们在 JavaScript 中优雅地处理错误。

如果你曾经写过这样的代码:

const user = getUserById(id);
if (user && user.profile) {
  return user.profile.name;
} else {
  return "Unknown User";
}

或者更糟的情况:

try {
  const result = riskyOperation();
  return result.data;
} catch (e) {
  return null;
}

你会发现这种模式重复、冗长、容易出错。而这就是我们今天要解决的问题:如何用函数式的方式统一处理“可能失败”的操作?


一、什么是 Functor(函子)?

首先我们要理解什么是 函子(Functor)

定义:

函子是一个包装了值的对象,它提供了一个 .map() 方法,可以对内部的值进行变换,同时保持结构不变。

换句话说,只要你的类型支持 .map(fn),并且满足两个数学性质(恒等律和组合律),你就有了一个函子。

在 JavaScript 中的例子:

// 简单的 Maybe 函子(伪实现)
class Maybe {
  constructor(value) {
    this.value = value;
  }

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

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

// 使用示例
const maybeUser = Maybe.of({ name: "Alice", age: 25 });
const maybeName = maybeUser.map(user => user.name); // Maybe("Alice")
const maybeNull = Maybe.of(null).map(user => user.name); // Maybe(null)

✅ 这里我们实现了 Maybemap,当值为 null/undefined 时自动返回空包装,避免后续调用出错。

这比手动检查 if (user) 更干净,也更容易链式调用。

操作 常规写法 Maybe 函子写法
获取用户姓名 user && user.name Maybe.of(user).map(u => u.name)
处理嵌套属性 user?.profile?.name Maybe.of(user).map(u => u.profile).map(p => p.name)

👉 函子的好处在于:你可以放心地连续 map,不用担心中间某个值是 null 或 undefined

但注意!函子只能处理“值存在与否”的情况,不能处理“错误类型”或“多种失败场景”。比如网络请求失败、数据库连接异常、参数格式不对……这些都属于“不同种类的失败”。

这就引出了下一个概念:单子(Monad)


二、什么是 Monad(单子)?

定义:

单子是一种特殊的函子,它还提供了一个 .chain().flatMap() 方法,允许你在值上执行带有副作用的操作(如异步、错误处理),并自动展开嵌套结构。

简单来说:

  • 函子:你可以在里面安全地做转换(map)。
  • 单子:你可以在里面安全地做流程控制(chain / flatMap),甚至跨多个失败路径。

为什么需要单子?

因为很多时候,你不是简单地想“如果为空就返回 null”,而是想:

  • 如果 A 成功 → 执行 B;
  • 如果 A 失败 → 返回错误信息;
  • 如果 B 又失败 → 返回另一个错误信息;

这就是 Either 单子的作用!


三、Maybe vs Either:谁更适合错误处理?

特性 Maybe Either
用途 表示“可能有值也可能没有” 表示“要么成功(Right),要么失败(Left)”
类型 Maybe<T> Either<Error, T>
是否携带错误信息 ❌ 否(只关心是否为空) ✅ 是(可区分具体错误)
链式操作 ✅ 支持 map ✅ 支持 chain(更强的流程控制)
实战场景 空值判断、默认值 API 调用、数据验证、文件读取等

🎯 推荐使用场景:

  • Maybe:当你只需要知道“有没有值”,不需要关心为什么没值。
  • Either:当你希望明确区分成功和失败,并且能传递具体的错误信息。

四、实战案例:用 Maybe 和 Either 处理真实业务逻辑

假设我们正在开发一个用户管理系统,涉及以下操作:

  1. 从数据库获取用户(可能不存在)
  2. 获取用户的地址(可能未设置)
  3. 发送邮件通知(可能失败)

场景一:使用 Maybe 处理空值

// Maybe 实现(简化版)
class Maybe {
  constructor(value) {
    this.value = value;
  }

  map(fn) {
    if (this.value == null) return Maybe.of(null);
    return Maybe.of(fn(this.value));
  }

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

// 模拟数据库查询
function getUser(id) {
  return Maybe.of(users.find(u => u.id === id));
}

function getAddress(userId) {
  return getUser(userId).map(user => user.address);
}

function sendEmail(address) {
  return address ? Maybe.of(`Email sent to ${address}`) : Maybe.of(null);
}

// 使用链式调用
const result = getUser(123)
  .map(u => u.profile)
  .map(profile => profile.email)
  .map(email => sendEmail(email));

console.log(result.value); // null(因为 profile 不存在)

✅ 这种方式避免了 Cannot read property 'email' of undefined 错误。

场景二:使用 Either 处理多种错误

现在我们升级需求:不仅要处理空值,还要记录错误原因。

// Either 实现(简化版)
class Either {
  constructor(leftOrRight) {
    this.isLeft = leftOrRight instanceof Left;
    this.value = leftOrRight.value;
  }

  map(fn) {
    if (this.isLeft) return this; // 左边不映射
    return Either.of(fn(this.value));
  }

  chain(fn) {
    if (this.isLeft) return this;
    return fn(this.value);
  }

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

  static left(error) {
    return new Left(error);
  }
}

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

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

// 模拟复杂业务逻辑
function fetchUser(id) {
  if (!id) return Either.left(new Error("ID is required"));
  const user = users.find(u => u.id === id);
  if (!user) return Either.left(new Error(`User not found for ID: ${id}`));
  return Either.of(user);
}

function validateEmail(user) {
  if (!user.email) return Either.left(new Error("Email missing"));
  return Either.of(user);
}

function sendNotification(user) {
  // 模拟发送失败
  if (Math.random() > 0.8) {
    return Either.left(new Error("Failed to send notification"));
  }
  return Either.of(`Notified ${user.email}`);
}

// 使用链式调用(chain 替代 map,适合错误传播)
const result = fetchUser(123)
  .chain(validateEmail)
  .chain(sendNotification);

console.log(result.isLeft ? result.value.message : result.value);
// 输出可能是:"Failed to send notification" 或 "Notified [email protected]"

🎯 这里 chain 允许我们在每个步骤中决定是否继续流程,而不是像 map 那样忽略错误。

💡 重点来了:chain 是单子的核心特性! 它让你可以写出类似下面的结构:

fetchUser(id)
  .chain(validateEmail)
  .chain(sendNotification)
  .fold(
    error => console.error("Error:", error.message),
    success => console.log("Success:", success)
  );

我们可以加一个 fold 方法来统一处理最终结果:

Either.prototype.fold = function(onLeft, onRight) {
  return this.isLeft ? onLeft(this.value) : onRight(this.value);
};

这样整个流程就可以清晰地分为两部分:错误处理成功处理


五、对比总结:Maybe vs Either 的选择指南

场景 推荐类型 理由
数据库查询、字段访问、API 返回值 Maybe 只关心是否存在,不需要详细错误信息
用户输入验证、HTTP 请求、文件操作 Either 必须知道失败原因,便于调试和用户体验
需要链式调用且中间可能失败 Either chain 支持错误传播,避免嵌套 try-catch
性能敏感、轻量级场景 Maybe 结构简单,无额外开销
日志追踪、错误分类 Either 可以携带丰富的错误对象(如 code、timestamp、context)

📌 小贴士:

  • 如果你发现很多地方都在写 if (x) { ... } else { return null },那就该考虑用 Maybe
  • 如果你发现很多地方都在写 try/catch 并抛出不同类型异常,那就该考虑用 Either

六、进阶技巧:结合两者构建健壮系统

有时候我们既需要处理“空值”,也需要处理“错误”,怎么办?

可以用 Either 包装 Maybe

function safeParseJSON(str) {
  try {
    return Either.of(JSON.parse(str));
  } catch (e) {
    return Either.left(new Error(`Invalid JSON: ${e.message}`));
  }
}

function extractUserId(data) {
  if (!data.user) return Either.left(new Error("No user in data"));
  return Either.of(data.user.id);
}

// 组合使用
safeParseJSON('{"user": {"id": 456}}')
  .chain(extractUserId)
  .fold(
    err => console.error(err.message),
    id => console.log("User ID:", id)
  );

这种模式非常适合微服务接口、配置加载器、数据清洗管道等场景。


七、常见误区与注意事项

误区 正确做法
“我用了 Maybe 就不用写 try/catch 了” ❌ 不对!Maybe 只能处理 null/undefined,无法捕获运行时异常(如语法错误、网络中断)
“Either 的 left 一定要是 Error 对象” ❌ 不一定!可以是字符串、对象、数字等,只要能表达错误含义即可
“链式调用越多越好” ❌ 应该合理分层,过度链式会让代码难以阅读。适当封装成函数更有意义
“单子太抽象,我不懂” ✅ 先从 Maybe 开始练手,再逐步过渡到 Either,慢慢体会其威力

八、结语:让错误变得可控,而不是被忽视

JavaScript 的动态性和灵活性带来了便利,但也让错误处理变得混乱。通过引入函子和单子的概念,我们可以:

  • 把“空值”变成一种结构化的状态(Maybe)
  • 把“错误”变成一种可传递的信息(Either)
  • 让整个流程变成纯函数式的链式操作,无需显式 try/catch

这不是为了炫技,而是为了让我们的代码更可预测、可测试、易维护

记住一句话:

“好的错误处理不是隐藏错误,而是让错误变得可见、可控、可恢复。”

下次当你遇到一堆 if (user)try/catch 时,请问问自己:“能不能用 Maybe 或 Either 来重构?”
你会发现,世界会变得更清晰一点。

谢谢大家!

发表回复

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