You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用Free Monad?求助在JavaScript的Monet库实现无副作用日志

用Monet.js实现基于Free Monad的无副作用日志

嘿,我完全懂你现在的困境——Free Monad本身就有点抽象,加上Monet库文档不够详细,对照Haskell的例子又因为术语和命名差异卡壳,确实让人头疼。我来一步步帮你用Monet.js实现无副作用的日志功能,尽量用JavaScript的思路讲清楚,避开不必要的Haskell术语。

核心思路先理清

Free Monad的关键就是把“要做什么”和“怎么做”彻底分开。对于日志功能来说:

  • 我们先定义“记录一条日志”这个操作的描述(完全是纯数据,没有副作用)
  • 再写一个解释器,决定这个描述具体怎么执行(比如输出到控制台、存到数组,甚至测试时mock掉)

这样程序本身只是一串操作描述,只有当我们用解释器运行它时,才会产生副作用——完美实现无副作用的日志逻辑。

步骤1:定义日志操作的函子

Free Monad要求我们的操作必须是一个函子(Functor),也就是要实现map方法。我们先定义一个简单的日志操作类型:

const { Free } = Monet;

// 定义日志操作的函子
const Log = message => ({
  message,
  // 函子必须实现map方法,这里日志操作不需要映射后续值,直接返回自身
  map: _ => Log(message)
});

// 把日志操作包装成Free Monad的一步
const log = message => Free.liftF(Log(message));

Free.liftF的作用就是把我们的Log函子转换成Free Monad的一个步骤,这样就能用Free的链式方法组合操作了。

步骤2:构建无副作用的程序

现在我们可以用log函数和Free的chain、pure方法来构建程序——这一步完全没有副作用,只是描述了我们要执行的操作序列:

// 示例程序:记录两条日志,最后返回一个结果
const program = log("程序启动")
  .chain(() => log("正在处理用户请求"))
  .chain(() => Free.pure("请求处理完成"));
  • chain:用来串联多个Free步骤,相当于“做完这一步,再做下一步”
  • Free.pure:把普通值(比如字符串)包装成Free的终止步骤,表示程序结束并返回这个值

步骤3:编写解释器执行程序

解释器是真正处理副作用的地方,我们可以写不同的解释器来实现不同的日志行为,而不需要修改程序本身。

解释器1:输出到控制台

这个解释器会把日志打印到控制台,用Monet的Identity函子来处理同步执行:

const { Identity } = Monet;

// 定义自然变换:把Log函子转换成Identity函子
const consoleLogInterpreter = logOp => {
  // 这里是唯一有副作用的地方:打印日志
  console.log(logOp.message);
  return Identity.of(null);
};

// 运行程序
program.foldMap(consoleLogInterpreter).value();

运行后你会在控制台看到两条日志,最后得到程序返回的"请求处理完成"。

解释器2:收集日志到数组(完全无副作用)

如果我们不想输出日志,而是把日志收集起来(比如测试时验证日志是否正确),可以用Monet的State monad写一个无副作用的解释器:

const { State } = Monet;

// 带状态的解释器:把日志添加到状态数组中
const collectLogsInterpreter = logOp => {
  return State(prevLogs => [
    prevLogs.concat(logOp.message), // 更新后的状态(日志数组)
    null // 当前步骤的返回值
  ]);
};

// 用初始空数组运行程序
const [collectedLogs, result] = program.foldMap(collectLogsInterpreter).run([]);

console.log("收集到的日志:", collectedLogs); // ["程序启动", "正在处理用户请求"]
console.log("程序结果:", result); // "请求处理完成"

这个解释器完全没有副作用,所有操作都是纯函数,非常适合测试场景。

关于Monet和Haskell的差异补充

  • Monet的Free.chain对应Haskell的>>=(bind),Free.pure对应Haskell的return
  • Haskell用代数数据类型(ADT)定义操作,JS里我们用对象模拟函子
  • Monet的foldMap和Haskell的foldMap作用一致:把Free结构转换成另一个Monad(比如Identity、State)执行

完整示例代码

把上面的代码整合起来,你可以直接运行测试:

const { Free, Identity, State } = Monet;

// 1. 定义日志函子和Free构造函数
const Log = message => ({
  message,
  map: _ => Log(message)
});
const log = message => Free.liftF(Log(message));

// 2. 构建程序
const program = log("程序启动")
  .chain(() => log("正在处理用户请求"))
  .chain(() => Free.pure("请求处理完成"));

// 3. 控制台解释器
console.log("=== 控制台日志输出 ===");
program.foldMap(logOp => {
  console.log(logOp.message);
  return Identity.of(null);
}).value();

// 4. 收集日志解释器
console.log("\n=== 收集日志结果 ===");
const [logs, result] = program.foldMap(logOp => 
  State(prev => [prev.concat(logOp.message), null])
).run([]);
console.log("日志列表:", logs);
console.log("程序返回值:", result);

如果还有具体的问题,比如怎么结合异步操作、扩展更多日志类型,或者Monet其他API的用法,随时问我!

内容的提问来源于stack exchange,提问作者Michael

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 09:19:23