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

时间:2016-01-19 15:29:54

标签: scala traits case-class self-type

我有一些案例类,其伴随对象中定义了方法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 个答案:

答案 0 :(得分:12)

因为Scala中的FunctionN类型之间没有关系,所以如果没有arity级别的样板,就不可能做到这一点 - 没有办法在不枚举所有方法的情况下对伴随对象的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方法扩展为函数。)

答案 1 :(得分:2)

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

我可以想象一种使用注释宏来切割样板的方法。由于使用标准库没有很好的方法,我将使用代码生成(仍具有编译时安全性)。需要注意的是,注释宏需要使用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的更好答案。

相关问题