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

Scala中Free monad代数返回Either时结果被Right包裹问题咨询

问题根源

你当前的代数定义将Either直接作为Command的返回值类型参数,因此通过C.ask拿到的name本身就是Either[Exception, String]实例,字符串插值时会调用其toString方法,自然会打印出Right(George)格式的内容。同时这种写法也没有利用到Free Monad的栈自动处理错误语义的能力,错误需要手动在业务代码里判断。

解决方案

这里提供三种常用的改法,可根据你的现有代码适配:

方案1:调整代数定义,将错误语义整合到Free Monad栈(推荐)

直接把Command的返回值参数设为原始类型,不要嵌套Either,错误能力通过Monad栈的MonadError提供:

// 修改Command定义,去掉返回值里的Either嵌套
sealed trait Command[A]
object Command {
  case class Tell(line: String) extends Command[Unit]
  case class Ask(line: String) extends Command[String]
}

// 你可以在App栈上叠加EitherT来承载错误,也可以给Free[App, *]实现MonadError实例
type App[A] = EitherK[Command, Log, A]
type Program[A] = EitherT[Free[App, *], Exception, A]

def program(implicit L: LogOps[App], C: CommandOps[App]): Program[Unit] =
  for {
    _ <- L.show("start <ask>").liftTo[Program]
    // 这里拿到的name直接是String类型
    name <- C.ask("What's your name?").liftTo[Program]
    _ <- L.show("start <tell>").liftTo[Program]
    _ <- C.tell(s"Hi <$name>, nice to meet you!").liftTo[Program]
    _ <- L.show("done.").liftTo[Program]
  } yield ()

解释器执行Command操作时如果发生错误,直接调用MonadError.raiseError抛出即可,整个流程会自动终止并返回Left。

方案2:不调整代数定义,在业务代码中手动展开Either

如果不想修改现有Command和Ops的定义,直接在for推导中处理Either即可:

import cats.syntax.all._
import cats.MonadError

def program(implicit L: LogOps[App],
            C: CommandOps[App],
            // 需给Free[App, *]提供MonadError实例
            ME: MonadError[Free[App, *], Exception]): Free[App, Unit] =
  for {
    _ <- L.show("start <ask>")
    nameEither <- C.ask("What's your name?")
    // 将Either转换为MonadError的错误效果,出错则直接终止流程
    name <- ME.fromEither(nameEither)
    _ <- L.show("start <tell>")
    _ <- C.tell(s"Hi <$name>, nice to meet you!")
    _ <- L.show("done.")
  } yield ()

如果需要自定义错误处理逻辑,也可以手动match:

name <- nameEither match {
  case Right(v) => Free.pure(v)
  case Left(e) => 
    // 比如打印错误日志然后终止流程
    L.show(s"ask failed: ${e.getMessage}") *> Free.raiseError(e)
}

方案3:调整CommandOps的lift逻辑,自动展开Either

如果不想改业务代码和Command定义,可以修改CommandOps的实现,在lift操作时自动把Either展开:

// 原来的ask实现大概是这样的
// def ask(line: String): Free[App, Either[Exception, String]] = liftF(Command.Ask(line))

// 修改后自动展开Either
def ask(line: String)(implicit ME: MonadError[Free[App, *], Exception]): Free[App, String] = 
  liftF(Command.Ask(line)).flatMap(ME.fromEither)

修改后上层业务代码不需要做任何调整,调用C.ask直接拿到String类型的返回值。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.15 12:45:04