在 scala 中通过 case class 定义要扩展的特征

Define a trait to be extended by case class in scala

我有一些案例 classes 在其伴生对象中定义了一个方法 tupled。从companion objects下面的代码可以看出,只是代码重复。

case class Book(id: Int, isbn: String, name: String)

object Book {
  def tupled = (Book.apply _).tupled // Duplication
}


case class Author(id: Int, name: String)

object Author {
  def tupled = (Author.apply _).tupled // Duplication
}

从另一个问题(can a scala self type enforce a case class type)来看,似乎我们不能将特征的自我类型强制为一个案例class。

有没有办法定义一个特征(比如Tupled)可以应用如下?

// What would be value of ???
trait Tupled {
  self: ??? =>

  def tupled = (self.apply _).tupled
}

// Such that I can replace tupled definition with Trait
object Book extends Tupled {
}

因为 Scala 中的 FunctionN 类型之间没有关系,如果没有 arity-level 某处的样板文件,就不可能做到这一点——只是没有办法对伴随对象的 apply 进行抽象方法而不枚举所有可能的成员数量。

您可以使用一堆 CompanionN[A, B, C, ...] 特征手动完成此操作,但这很烦人。 Shapeless 提供了一个更好的解决方案,它允许您编写如下内容:

import shapeless.{ Generic, HList }, shapeless.ops.product.ToHList

class CaseClassCompanion[C] {
  def tupled[P <: Product, R <: HList](p: P)(implicit
    gen: Generic.Aux[C, R],
    toR: ToHList.Aux[P, R]
  ): C = gen.from(toR(p))
}

然后:

case class Book(id: Int, isbn: String, name: String)
object Book extends CaseClassCompanion[Book]

case class Author(id: Int, name: String)
object Author extends CaseClassCompanion[Author]

你可以这样使用:

scala> Book.tupled((0, "some ISBN", "some name"))
res0: Book = Book(0,some ISBN,some name)

scala> Author.tupled((0, "some name"))
res1: Author = Author(0,some name)

您甚至可能不需要 CaseClassCompanion 部分,因为可以构造一个通用方法将元组转换为大小写 类(假设成员类型排列):

class PartiallyAppliedProductToCc[C] {
  def apply[P <: Product, R <: HList](p: P)(implicit
    gen: Generic.Aux[C, R],
    toR: ToHList.Aux[P, R]
  ): C = gen.from(toR(p))
}

def productToCc[C]: PartiallyAppliedProductToCc[C] =
  new PartiallyAppliedProductToCc[C]

然后:

scala> productToCc[Book]((0, "some ISBN", "some name"))
res2: Book = Book(0,some ISBN,some name)

scala> productToCc[Author]((0, "some name"))
res3: Author = Author(0,some name)

这将适用于最多有 22 个成员的情况 类(因为伴随对象上的 apply 方法不能 eta-expanded 到一个函数,如果有超过22 个参数)。

问题在于 apply 的签名因情况而异,并且这些函数没有共同特征。 Book.tupledAuthor.tupled 基本上具有相同的代码,但具有非常不同的签名。因此,解决方案可能没有我们想要的那么好。


我可以想到一种使用注释宏来剪切样板的方法。由于没有使用标准库的好方法,我将求助于代码生成(它仍然具有 compile-time 安全性)。这里需要注意的是注释宏需要使用 macro paradise 编译器插件。宏也必须在一个单独的编译单元中(就像另一个 sbt sub-project)。使用注释的代码还需要使用宏天堂插件。

import scala.annotation.{ StaticAnnotation, compileTimeOnly }
import scala.language.experimental.macros
import scala.reflect.macros.whitebox.Context

@compileTimeOnly("enable macro paradise to expand macro annotations")
class Tupled extends StaticAnnotation {
  def macroTransform(annottees: Any*): Any = macro tupledMacroImpl.impl
}

object tupledMacroImpl {

  def impl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
    import c.universe._
    val result = annottees map (_.tree) match {
      // A case class with companion object, we insert the `tupled` method into the object
      // and leave the case class alone.
      case (classDef @ q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }")
        :: (objDef @ q"object $objName extends { ..$objEarlyDefs } with ..$objParents { $objSelf => ..$objDefs }")
        :: Nil if mods.hasFlag(Flag.CASE) =>
        q"""
          $classDef
          object $objName extends { ..$objEarlyDefs } with ..$objParents { $objSelf => 
            ..$objDefs
            def tupled = ($objName.apply _).tupled
          }
        """
      case _ => c.abort(c.enclosingPosition, "Invalid annotation target: must be a companion object of a case class.")
    }

    c.Expr[Any](result)
  }

}

用法:

@Tupled
case class Author(id: Int, name: String)

object Author


// Exiting paste mode, now interpreting.

defined class Author
defined object Author

scala> Author.tupled
res0: ((Int, String)) => Author = <function1>

或者,对于 shapeless 来说,这样的事情可能是可能的。请参阅@TravisBrown 的更好答案。