动态混入一个特质。

37
拥有某种特性
trait Persisted {
  def id: Long
}

如何实现一个接受任何样例类实例并返回混入该特质的副本的方法?

该方法的签名如下:

def toPersisted[T](instance: T, id: Long): T with Persisted

这是一个有趣的问题,但冒着陈述显而易见的风险,为什么你的 case 类没有扩展一个提供 id 的公共 trait 呢? - virtualeyes
@virtualeyes 这是我正在处理的一个ORM非常精细调试的API问题。在这些对象实现了这个特征之前,它们仅仅是业务逻辑对象,没有任何关于数据库的引用,但这种方法需要一个API方法 def save[T](data: T): T with Persisted,该方法依赖于问题描述中所述的方法。 - Nikita Volkov
好的,你有你的理由,但到目前为止的答案都表明,在Scala中,你可能需要重新考虑你的方法。你正在使用哪个ORM,是你自己编写的还是第三方的? - virtualeyes
@virtualeyes 这是我正在开发的一个新ORM项目。我不认为它是不可能的,只是认为这将会很棘手,可能需要涉及字节码操作。一旦找到解决方案,我就会发布或在这里选择一个。Emil H提出了一个不错的建议,我会尝试进一步完善它。 - Nikita Volkov
啊啊啊,自己编写的乐趣;-)使用Emil H的方法,在编译时如何执行“new T with Persisted”?似乎你需要一个大型的match {}语句(即手动指定目标类),那么如果是这种情况,为什么不直接提供一个id呢?嘿嘿,你会想出来的,或者放弃并选择ScalaQuery ;-) - virtualeyes
有趣的是,我们都在使用Slick时遇到了同样的问题,但typesafe并没有在他们的学术思维模式中提供一个开箱即用的解决方案。 - SkyWalker
5个回答

32

这可以通过宏来实现(自 Scala 2.10.0-M3 起正式成为 Scala 的一部分)。以下是一个示例代码

1) 我的宏生成一个本地类,该类继承提供的 case class 和 Persisted,很像 new T with Persisted。然后它会缓存参数(以防止多次评估)并创建所创建类的实例。

2) 我是如何知道要生成什么树形结构的呢?我有一个简单的应用程序 parse.exe,它打印从输入代码解析后得到的 AST。因此,我只需调用 parse class Person$Persisted1(first: String, last: String) extends Person(first, last) with Persisted,记录输出并在我的宏中重现它即可。parse.exe 是 scalac -Xprint:parser -Yshow-trees -Ystop-after:parser 的包装器。有不同的方法来探索 AST,请阅读 "Scala 2.10 中的元编程"

3) 如果您在scalac作为参数中提供-Ymacro-debug-lite,则可以对宏扩展进行合理性检查。在这种情况下,所有扩展都将被打印出来,您将能够更快地检测到代码生成错误。

编辑。更新了2.10.0-M7的示例


虽然这些东西非常有趣,但也非常难懂。我该如何将这几个编译阶段与Maven集成?当发布到达时,它会变得更易于理解吗? - Nikita Volkov
我认为它可以在maven中无问题地工作,你可以发送编译器参数:http://scala-tools.org/mvnsites/maven-scala-plugin/example_compile.html。我有一个基于expectify的gradle工作示例:https://github.com/flatMapDuke/TestMacro/commit/9a8949a1637ef967b9de45f31dd034d2506775a0。我对树形操作有点印象深刻,但这太棒了 :) - jwinandy
3
我同意,分离式编译确实不太方便。我会尽力解决这个问题。 - Eugene Burmako

9

使用纯Scala无法实现您想要的功能。问题在于像以下这样的混入:

scala> class Foo
defined class Foo

scala> trait Bar
defined trait Bar

scala> val fooWithBar = new Foo with Bar
fooWithBar: Foo with Bar = $anon$1@10ef717

创建一个混入了Bar的Foo对象,但这并不是在运行时完成的。编译器只会简单地生成一个新的匿名类:
scala> fooWithBar.getClass
res3: java.lang.Class[_ <: Foo] = class $anon$1

有关更多信息,请参见Scala中的动态混入 - 是否可能?


@NikitaVolkov 你也可以看一下 Autoproxy。https://github.com/scala-incubator/autoproxy-plugin/wiki 但我不确定它的当前状态。 - Emil L
你可以期待在2.11版本发布时会有一个完全基于宏的版本可用,希望我能及时准备好RC1。 - Kevin Wright

4

更新

您可以找到一个最新的工作解决方案,该解决方案利用Scala 2.10.0-RC1的Toolboxes API作为SORM项目的一部分。


以下解决方案基于Scala 2.10.0-M3反射API和Scala解释器。它动态创建并缓存从原始case类继承混合trait的类。由于最大化缓存,此解决方案应该仅为每个原始case类动态创建一个类,并在以后重用它。
由于新的反射API没有披露太多信息,也不稳定,并且还没有关于它的教程,因此此解决方案可能涉及一些愚蠢的重复操作和怪癖。
以下代码已在Scala 2.10.0-M3中经过测试。
1. Persisted.scala
要混合的trait。请注意,由于程序更新,我对其进行了一些更改。
trait Persisted {
  def key: String
}

2. PersistedEnabler.scala

实际的工作对象

import tools.nsc.interpreter.IMain
import tools.nsc._
import reflect.mirror._

object PersistedEnabler {

  def toPersisted[T <: AnyRef](instance: T, key: String)
                              (implicit instanceTag: TypeTag[T]): T with Persisted = {
    val args = {
      val valuesMap = propertyValuesMap(instance)
      key ::
        methodParams(constructors(instanceTag.tpe).head.typeSignature)
          .map(_.name.decoded.trim)
          .map(valuesMap(_))
    }

    persistedClass(instanceTag)
      .getConstructors.head
      .newInstance(args.asInstanceOf[List[Object]]: _*)
      .asInstanceOf[T with Persisted]
  }


  private val persistedClassCache =
    collection.mutable.Map[TypeTag[_], Class[_]]()

  private def persistedClass[T](tag: TypeTag[T]): Class[T with Persisted] = {
    if (persistedClassCache.contains(tag))
      persistedClassCache(tag).asInstanceOf[Class[T with Persisted]]
    else {
      val name = generateName()

      val code = {
        val sourceParams =
          methodParams(constructors(tag.tpe).head.typeSignature)

        val newParamsList = {
          def paramDeclaration(s: Symbol): String =
            s.name.decoded + ": " + s.typeSignature.toString
          "val key: String" :: sourceParams.map(paramDeclaration) mkString ", "
        }
        val sourceParamsList =
          sourceParams.map(_.name.decoded).mkString(", ")

        val copyMethodParamsList =
          sourceParams.map(s => s.name.decoded + ": " + s.typeSignature.toString + " = " + s.name.decoded).mkString(", ")

        val copyInstantiationParamsList =
          "key" :: sourceParams.map(_.name.decoded) mkString ", "

        """
        class """ + name + """(""" + newParamsList + """)
          extends """ + tag.sym.fullName + """(""" + sourceParamsList + """)
          with """ + typeTag[Persisted].sym.fullName + """ {
            override def copy(""" + copyMethodParamsList + """) =
              new """ + name + """(""" + copyInstantiationParamsList + """)
          }
        """
      }

      interpreter.compileString(code)
      val c =
        interpreter.classLoader.findClass(name)
          .asInstanceOf[Class[T with Persisted]]

      interpreter.reset()

      persistedClassCache(tag) = c

      c
    }
  }

  private lazy val interpreter = {
    val settings = new Settings()
    settings.usejavacp.value = true
    new IMain(settings, new NewLinePrintWriter(new ConsoleWriter, true))
  }


  private var generateNameCounter = 0l

  private def generateName() = synchronized {
    generateNameCounter += 1
    "PersistedAnonymous" + generateNameCounter.toString
  }


  // REFLECTION HELPERS

  private def propertyNames(t: Type) =
    t.members.filter(m => !m.isMethod && m.isTerm).map(_.name.decoded.trim)

  private def propertyValuesMap[T <: AnyRef](instance: T) = {
    val t = typeOfInstance(instance)

    propertyNames(t)
      .map(n => n -> invoke(instance, t.member(newTermName(n)))())
      .toMap
  }

  private type MethodType = {def params: List[Symbol]; def resultType: Type}

  private def methodParams(t: Type): List[Symbol] =
    t.asInstanceOf[MethodType].params

  private def methodResultType(t: Type): Type =
    t.asInstanceOf[MethodType].resultType

  private def constructors(t: Type): Iterable[Symbol] =
    t.members.filter(_.kind == "constructor")

  private def fullyQualifiedName(s: Symbol): String = {
    def symbolsTree(s: Symbol): List[Symbol] =
      if (s.enclosingTopLevelClass != s)
        s :: symbolsTree(s.enclosingTopLevelClass)
      else if (s.enclosingPackageClass != s)
        s :: symbolsTree(s.enclosingPackageClass)
      else
        Nil

    symbolsTree(s)
      .reverseMap(_.name.decoded)
      .drop(1)
      .mkString(".")
  }

}

3. Sandbox.scala

测试应用程序

import PersistedEnabler._

object Sandbox extends App {
  case class Artist(name: String, genres: Set[Genre])
  case class Genre(name: String)

  val artist = Artist("Nirvana", Set(Genre("rock"), Genre("grunge")))

  val persisted = toPersisted(artist, "some-key")

  assert(persisted.isInstanceOf[Persisted])
  assert(persisted.isInstanceOf[Artist])
  assert(persisted.key == "some-key")
  assert(persisted.name == "Nirvana")
  assert(persisted == artist)  //  an interesting and useful effect

  val copy = persisted.copy(name = "Puddle of Mudd")

  assert(copy.isInstanceOf[Persisted])
  assert(copy.isInstanceOf[Artist])
  //  the only problem: compiler thinks that `copy` does not implement `Persisted`, so to access `key` we have to specify it manually:
  assert(copy.asInstanceOf[Artist with Persisted].key == "some-key")
  assert(copy.name == "Puddle of Mudd")
  assert(copy != persisted)

}

1
如果您对宏不太熟悉,可以使用新的工具箱API来编译AST,并且与解释器不同,它保证向后兼容。您可以复制/粘贴我的树操作代码,然后使用scala.reflect.mirror.mkToolBox().runExpr(...)来编译和运行它。 - Eugene Burmako
还有,什么东西让人感觉像黑魔法一样?只是需要与构建工具集成,还是其他原因? - Eugene Burmako
@EugeneBurmako 非常感谢您对工具箱的建议,我一定会去看看!关于黑魔法。如果没有像样的教程或文档,很难掌握发生了什么。此外,构建AST的API似乎是为了造成痛苦而开发的,虽然我对反射API也没有更好的评价,但我知道这都是由于与编译器世界的混合所致。在此基础上手动管理编译过程就太多了,我最好将这个问题留给未解决。 - Nikita Volkov

4
你尝试的是记录连接,这是Scala类型系统不支持的。 (顺便说一下,存在类型系统 - 例如 - 提供此功能。)
我认为类型类可能适合您的用例,但由于问题没有提供足够的信息来解决问题,我无法确定。

1
实际上,你可以在Scala的类型系统中编码可扩展记录,但恐怕这并不能直接帮助回答这个问题。 - Miles Sabin
是的,我知道。我们之前谈过这个问题。 - missingfaktor
好的,但是你在回答中说了完全相反的话? - Miles Sabin
这些记录可以在Scala中进行编码。但这与它们作为语言中的一级构造有所不同。 - missingfaktor
你不能在Scala中连接类/特质,这就是我的意思。(我认为从上下文中很明显吧?) - missingfaktor
1
在完全的广泛性上?不行。但您可以例如连接元组。 - Miles Sabin

-1

虽然在对象创建之后无法组合对象,但是您可以使用类型别名和定义结构体进行非常广泛的测试,以确定对象是否具有特定的组合:

  type Persisted = { def id: Long }

  class Person {
    def id: Long = 5
    def name = "dude"
  }

  def persist(obj: Persisted) = {
    obj.id
  }

  persist(new Person)

任何带有 def id:Long 的对象都可以被视为持久化对象。
通过隐式转换可以实现我认为你正在尝试做的事情。
  object Persistable {
    type Compatible = { def id: Long }
    implicit def obj2persistable(obj: Compatible) = new Persistable(obj)
  }
  class Persistable(val obj: Persistable.Compatible) {
    def persist() = println("Persisting: " + obj.id)
  }

  import Persistable.obj2persistable
  new Person().persist()

抱歉,但这与问题有点无关。 - Nikita Volkov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接