首页 文章

定义要在scala中按案例类扩展的特征

提问于
浏览
7

我有一些case类,它的伴随对象中定义了一个方法 tupled . 从下面的代码对象中可以看出,它只是代码重复 .

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)来看,似乎我们不能强制将特征的自我类型作为案例类 .

有没有办法定义可以如下应用的特征(比如 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 {
}

2 回答

  • 2

    因为Scala中的 FunctionN 类型之间没有任何关系,所以's not possible to do this without arity-level boilerplate somewhere—there'只是无法在不枚举所有可能的成员数的情况下对伴随对象的 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个成员的案例类(因为如果有超过22个参数,则无法将伴随对象上的 apply 方法扩展为函数) .

  • 12

    问题是 apply 的签名因具体情况而异,并且这些功能没有共同特征 . Book.tupledAuthor.tupled 基本上具有相同的代码,但具有非常不同的签名 . 因此,解决方案可能不如我们所希望的那么好 .


    我可以设想一种使用注释宏来切割样板的方法 . 由于没有't a nice way to do it with the standard library, I' ll求助于代码生成(它仍具有编译时安全性) . 需要注意的是,注释宏需要使用macro paradise编译器插件 . 宏也必须位于单独的编译单元中(如另一个sbt子项目) . 使用注释的代码也需要使用宏天堂插件 .

    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>
    

    或者,这样的事情可能是无形的 . 请参阅@ TravisBrown的更好答案 .

相关问题