Scala中如何以函数式方式修改基类字段
纯函数式修改Scala Case Class的Tag字段并保留类型
背景与需求
现有如下类层次:
trait Base { val tag: String } case class Derived1(tag: String = "Derived 1") extends Base case class Derived2(tag: String = "Derived 2") extends Base // etc ...
需要定义一个方法,接收任意Base子类实例,修改其tag字段后返回同类型的新实例,方法签名如下:
def tag[T <: Base](instance: T, tag: String): T
用可变变量var tag: String很容易实现,但希望找到纯函数式的解决方案。
现有思路的问题
最初尝试用类型类Tagger来实现:
trait Tagger[T] { def tag(t: T, state: String): T } implicit object TaggerDerived1 extends Tagger[Derived1] { override def tag(t: Derived1, state: String): Derived1 = t.copy(tag = state) } implicit object TaggerDerived2 extends Tagger[Derived2] { override def tag(t: Derived2, state: String): Derived2 = t.copy(tag = state) } implicit object TaggerBase extends Tagger[Base] { override def tag(t: Base, state: String): Base = ??? } def tag[T <: Base](instance: T, tag: String)(implicit tagger: Tagger[T]): T = tagger.tag(instance, tag)
但这个方案有明显缺陷:用户新定义子类时必须手动编写对应的Tagger实例,否则隐式解析会回退到TaggerBase,导致返回类型被收窄为Base而非子类本身:
case class Derived3(tag: String = "Derived 3") extends Base tag(Derived3(), "test") // 返回类型为Base,而非预期的Derived3
纯函数式解决方案
方案1:Scala 2 + Shapeless 自动生成类型类实例
借助Shapeless的LabelledGeneric,可以自动为所有带tag字段的Base子类生成Tagger实例,无需用户手动编写:
首先添加Shapeless依赖(以sbt为例):
libraryDependencies += "com.chuusai" %% "shapeless" % "2.3.10"
然后实现通用Tagger:
import shapeless._ import shapeless.labelled.FieldType trait Tagger[T] { def tag(t: T, newTag: String): T } object Tagger { // 自动为带tag字段的Base子类生成Tagger实例 implicit def autoTagger[T <: Base, Repr <: HList]( implicit gen: LabelledGeneric.Aux[T, Repr], upd: ops.hlist.Modifier.Aux[Repr, Symbol @@ "tag", String, Repr] ): Tagger[T] = new Tagger[T] { override def tag(t: T, newTag: String): T = { val originalRepr = gen.to(t) val updatedRepr = upd(originalRepr, newTag) gen.from(updatedRepr) } } } // 使用方式 def tag[T <: Base](instance: T, newTag: String)(implicit tagger: Tagger[T]): T = tagger.tag(instance, newTag) // 新子类无需额外定义Tagger case class Derived3(tag: String = "Derived 3") extends Base val updated = tag(Derived3(), "test") // updated类型为Derived3
方案2:Scala 3 内置Mirror派生
Scala 3的内置Mirror和元编程能力,不需要额外依赖就能自动生成实例:
trait Tagger[T] { def tag(t: T, newTag: String): T } object Tagger { // 自动派生带tag字段的Base子类的Tagger实例 inline given taggerForBaseSubclass[T <: Base](using m: Mirror.ProductOf[T]): Tagger[T] = new Tagger[T] { override def tag(t: T, newTag: String): T = { val fieldMap = m.productElementNames.zip(m.product(t)).toMap val updatedFields = fieldMap.updated("tag", newTag) m.fromProduct(Tuple.fromArray(updatedFields.values.toArray)) } } } // 通用方法 def tag[T <: Base](instance: T, newTag: String)(using Tagger[T]): T = summon[Tagger[T]].tag(instance, newTag) // 使用示例 case class Derived3(tag: String = "Derived 3") extends Base val updated = tag(Derived3(), "test") // updated类型为Derived3
方案3:Base trait定义抽象方法
在Base中定义一个抽象方法withTag,让子类利用case class自动生成的copy方法实现:
trait Base { val tag: String def withTag(newTag: String): this.type } // 子类实现(case class的copy方法自动生成,直接复用) case class Derived1(tag: String = "Derived 1") extends Base { override def withTag(newTag: String): this.type = copy(tag = newTag).asInstanceOf[this.type] } case class Derived2(tag: String = "Derived 2") extends Base { override def withTag(newTag: String): this.type = copy(tag = newTag).asInstanceOf[this.type] } // 通用方法 def tag[T <: Base](instance: T, newTag: String): T = instance.withTag(newTag) // 新子类只需实现withTag case class Derived3(tag: String = "Derived 3") extends Base { override def withTag(newTag: String): this.type = copy(tag = newTag).asInstanceOf[this.type] } val updated = tag(Derived3(), "test") // updated类型为Derived3
总结
- Scala 2推荐用Shapeless方案,无需子类做额外工作,完全自动生成实例
- Scala 3推荐用内置Mirror方案,零依赖且更简洁
- 若不想引入额外依赖,可选择Base trait定义抽象方法的方案,只需子类简单实现
withTag
内容的提问来源于stack exchange,提问作者David Tomecek
相关产品推荐
相关产品推荐

