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

如何在Scala trait层面实现错误类withContext?避免样板代码

问题

我有一套错误继承体系,因为存在菱形继承结构,错误类必须扩展trait。很多场景下需要在错误生成位置的上层调用站点为其附加上下文,示例代码如下:

trait MyError{
  def getMessage : String
  val context : String
  override def toString = context + getMessage
  def withContext(newContext : String) : MyError
}
case class IntError(value : Int, context : String = "") extends MyError{
  def getMessage = s"$value is not a good enough int."
  def withContext(newContext : String) = this.copy(context = context+ newContext + "\n")
  
}
case class StringError(value : String, context: String = "") extends MyError{
  def getMessage = s"Error Encountered: $value"
  def withContext(newContext : String) = this.copy(context = context + newContext+ "\n")
}
def bar (i : Int, b : Boolean) : Either[MyError, Int] = {
  if (b) then {
    if (i < 0) then Left(IntError(i)) else Right(i)
  } else Left(StringError("That needed to be True."))
}
def foo() : Either[MyError, Int] = bar(-2, true)
    .left.map(_.withContext("Error calling bar from foo:"))
def baz() : Either[MyError, Int] = bar(3, false)
    .left.map(_.withContext("Error calling bar from baz:"))

println(foo().left.get)
println(baz().left.get)

执行结果符合预期:

Error calling bar from foo:
-2 is not a good enough int.
Error calling bar from baz:
Error Encountered: That needed to be True.

但因为case类数量很多,不想为每个类编写withContext的样板代码。请问能否将该方法的实现移至trait层面?额外约束/优化目标:

  • 不可使用可变操作;
  • MyError需保留为trait(因存在菱形继承结构,可接受合理变通方案);
  • 若context无需在每个case类中定义则更佳;
  • withContext最好返回具体的case类类型而非通用的MyError(非必须)。

解决方案

方案1:泛型+自类型实现通用逻辑,统一管理context

通过泛型绑定和自类型约束,让trait MyError知晓子类是带context字段的case类,同时将context统一放在trait中,避免子类重复定义:

trait MyError[E <: MyError[E]] {
  self: E =>
  def getMessage: String
  val context: String = ""
  override def toString = context + getMessage
  
  // 复用case类自动生成的copy方法,实现通用的withContext
  def withContext(newContext: String): E = {
    val updatedContext = context + newContext + "\n"
    this.copy(context = updatedContext)
  }
  
  // 定义copy方法签名,强制子类(case类)实现(case类会自动生成符合要求的copy)
  def copy(context: String): E
}

// 子类只需实现getMessage,无需手动编写withContext
case class IntError(value: Int, override val context: String = "") extends MyError[IntError] {
  def getMessage = s"$value is not a good enough int."
}

case class StringError(value: String, override val context: String = "") extends MyError[StringError] {
  def getMessage = s"Error Encountered: $value"
}

// 原业务逻辑保持不变
def bar(i: Int, b: Boolean): Either[MyError[_], Int] = {
  if (b) {
    if (i < 0) Left(IntError(i)) else Right(i)
  } else Left(StringError("That needed to be True."))
}

def foo(): Either[MyError[_], Int] = bar(-2, true)
  .left.map(_.withContext("Error calling bar from foo:"))

def baz(): Either[MyError[_], Int] = bar(3, false)
  .left.map(_.withContext("Error calling bar from baz:"))

println(foo().left.get)
println(baz().left.get)

该方案满足所有约束:

  • 无可变操作,通过copy创建新实例实现上下文追加;
  • MyError保持为trait,泛型+自类型可适配菱形继承场景;
  • context仅在trait中定义,子类仅需按需override默认值;
  • withContext返回具体case类类型,保证类型安全。

方案2:类型类解耦copy逻辑(可选)

如果不想让MyError带泛型,可通过类型类封装copy逻辑,进一步解耦:

trait MyError {
  def getMessage: String
  val context: String = ""
  override def toString = context + getMessage
  def withContext(newContext: String): MyError
}

// 定义类型类,处理带context字段的case类复制逻辑
trait ContextCopier[E <: MyError] {
  def copyWithContext(error: E, newContext: String): E
}

// 自动为符合条件的case类生成ContextCopier实例
object ContextCopier {
  implicit def forCaseClass[E <: MyError](implicit ev: E <:< { def copy(context: String): E }): ContextCopier[E] =
    (error: E, newContext: String) => {
      val updatedContext = error.context + newContext + "\n"
      error.copy(context = updatedContext)
    }
}

// 扩展MyError,提供withContext的默认实现
trait MyErrorWithContext extends MyError {
  self: E =>
  def withContext(newContext: String)(implicit copier: ContextCopier[E]): E =
    copier.copyWithContext(this, newContext)
}

// 子类只需继承MyErrorWithContext,无需额外实现
case class IntError(value: Int, override val context: String = "") extends MyErrorWithContext {
  def getMessage = s"$value is not a good enough int."
}

case class StringError(value: String, override val context: String = "") extends MyErrorWithContext {
  def getMessage = s"Error Encountered: $value"
}

// 业务逻辑使用方式不变
def bar(i: Int, b: Boolean): Either[MyError, Int] = {
  if (b) {
    if (i < 0) Left(IntError(i)) else Right(i)
  } else Left(StringError("That needed to be True."))
}

def foo(): Either[MyError, Int] = bar(-2, true)
  .left.map(_.withContext("Error calling bar from foo:"))

def baz(): Either[MyError, Int] = bar(3, false)
  .left.map(_.withContext("Error calling bar from baz:"))

println(foo().left.get)
println(baz().left.get)

这个方案同样满足所有约束,且MyError的泛型被隐藏在类型类中,对调用者更透明。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 07:47:21