Scalaz中Free Monad实践:如何减少提取器对象代码冗余?
嘿,很高兴看到你在折腾Scalaz的Free Monad!这种重复写提取器的问题确实很常见,尤其是刚上手定义代数的时候。咱们来聊聊怎么把这些冗余代码干掉,同时贴合Scalaz的风格~
首先,先还原一下我猜你可能写的冗余代码(应该和这个差不多吧?):
import scalaz._ import Scalaz._ import Free._ // 你的AST代数 sealed trait Expr[A] case class Inc[A](next: Free[Expr, A]) extends Expr[A] case class Dec[A](next: Free[Expr, A]) extends Expr[A] case class Val[A](value: Int) extends Expr[A] // 冗余的提取器对象 object Inc { def apply[A](a: Free[Expr, A]): Free[Expr, A] = liftF(Inc(a)) def unapply[A](fa: Free[Expr, A]): Option[Free[Expr, A]] = fa match { case Free.FreeS(inc: Inc[A]) => Some(inc.next) case _ => None } } // Dec、Val也有几乎一样的apply/unapply,重复度拉满
核心思路:别手动写提取器,让Scalaz帮你做
Scalaz的Free Monad设计本来就不推荐手动为每个代数构造器写提取器。咱们可以从两个方向优化:
1. 重构代数定义,用liftF替代手动提取器
首先把你的AST改成“基于操作的代数”,而不是递归嵌套的结构——这是Free Monad的标准玩法:
import scalaz._ import Scalaz._ import Free._ // 定义单个操作的ADT,每个操作代表一步计算 sealed trait ExprOp[A] case class Inc(n: Int) extends ExprOp[Int] // 对n执行递增,输出Int case class Dec(n: Int) extends ExprOp[Int] // 对n执行递减,输出Int case class Val(v: Int) extends ExprOp[Int] // 直接返回值v,输出Int // 为每个操作写辅助构造函数,用liftF自动包装成Free实例 def inc(n: Int): Free[ExprOp, Int] = liftF(Inc(n)) def dec(n: Int): Free[ExprOp, Int] = liftF(Dec(n)) def value(v: Int): Free[ExprOp, Int] = liftF(Val(v))
这样一来,你完全不需要写Inc/Dec/Val的提取器对象——构造Free实例用上面的辅助函数就行,而解释器直接匹配原始的case class:
// 定义解释器:把ExprOp转换成Id(纯值) val exprInterpreter: ExprOp ~> Id = new (ExprOp ~> Id) { def apply[A](op: ExprOp[A]): Id[A] = op match { case Inc(n) => n + 1 case Dec(n) => n - 1 case Val(v) => v } } // 测试程序:用for-comprehension组合操作 val program = for { a <- value(5) b <- inc(a) c <- dec(b) } yield c // 运行解释器 program.foldMap(exprInterpreter) // 结果是5
2. 如果需要组合多个子代数,用Inject+Coproduct避免重复
如果你的表达式以后要扩展更多操作(比如加减乘除),可以把代数拆成多个子模块,用Scalaz的Inject和Coproduct组合,彻底消除重复的lift逻辑:
// 子代数1:增减操作 sealed trait NumOp[A] case class Inc(n: Int) extends NumOp[Int] case class Dec(n: Int) extends NumOp[Int] // 子代数2:值操作 sealed trait ValueOp[A] case class Val(v: Int) extends ValueOp[Int] // 组合代数:把NumOp和ValueOp合并成一个类型 type ExprOp[A] = Coproduct[NumOp, ValueOp, A] // 用Inject自动lift子代数的操作到Free中,不用重复写liftF def inc(n: Int)(implicit I: Inject[NumOp, ExprOp]): Free[ExprOp, Int] = liftF(I.inj(Inc(n))) def dec(n: Int)(implicit I: Inject[NumOp, ExprOp]): Free[ExprOp, Int] = liftF(I.inj(Dec(n))) def value(v: Int)(implicit I: Inject[ValueOp, ExprOp]): Free[ExprOp, Int] = liftF(I.inj(Val(v))) // 分别定义子代数的解释器 val numInterpreter: NumOp ~> Id = new (NumOp ~> Id) { def apply[A](op: NumOp[A]): Id[A] = op match { case Inc(n) => n +1 case Dec(n) => n -1 } } val valueInterpreter: ValueOp ~> Id = new (ValueOp ~> Id) { def apply[A](op: ValueOp[A]): Id[A] = op match { case Val(v) => v } } // 组合解释器 val exprInterpreter: ExprOp ~> Id = numInterpreter or valueInterpreter
为什么这更符合Scalaz风格?
Scalaz的Free Monad核心是分离“代数定义”和“解释逻辑”:
- 代数只关心有哪些操作,不关心操作怎么执行
- 解释器负责具体实现每个操作的逻辑
- 用
liftF和Inject自动处理Free的包装,完全不需要手动写提取器来拆包/打包
这样你的代码会更简洁、更易扩展,也完全贴合Scalaz的函数式设计思路~
内容的提问来源于stack exchange,提问作者St.Antario
相关产品推荐
相关产品推荐

