动态混合特征

Posted

技术标签:

【中文标题】动态混合特征【英文标题】:Mixing in a trait dynamically 【发布时间】:2012-05-09 12:58:50 【问题描述】:

有特点

trait Persisted 
  def id: Long

我如何实现一个方法,该方法接受任何案例类的实例并返回其副本以及混合的特征?

方法的签名如下:

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

【问题讨论】:

这是一个有趣的问题,但冒着说明显而易见的风险,为什么您的案例类没有扩展提供 id 的共同特征?? @virtualeyes 这是我正在处理的 ORM 的一个非常精细调整的 API 的问题。在这些对象实现此特征之前,它们只是不引用 db 的业务逻辑对象,但这种方法需要像 def save[T](data: T): T with Persisted 这样的 API 方法,这将依赖于问题中描述的方法。 好吧,你有你的理由,但迄今为止的答案都表明,在 Scala 中你可能不得不重新考虑你的方法。您正在使用什么 ORM,您编写的 ORM 还是第 3 方? @virtualeyes 这是我正在做的一个新的 ORM 项目。我不认为这是不可能的,我只是认为这会很困难,可能会涉及到弄乱字节码。一旦出现解决方案,我将在此处发布或选择一个。 Emil H 提出了一个很好的建议,我会尝试改进。 ahhhh,滚动自己的乐趣 ;-) 使用 Emil H 的方法,您将如何在编译时执行“new T with Persisted”?似乎您需要一个大量的 match 语句(即手动指定目标类),然后如果是这种情况,那么为什么不只提供一个 id?呵呵,你会想办法的,或者放弃使用 ScalaQuery ;-) 【参考方案1】:

使用 vanilla 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

创建一个Foo with Bar 混入,但不是在运行时完成。编译器只是生成一个新的匿名类:

scala> fooWithBar.getClass
res3: java.lang.Class[_ <: Foo] = class $anon$1

请参阅Dynamic mixin in Scala - is it possible? 了解更多信息。

【讨论】:

@NikitaVolkov 你也可以看看 Autoproxy。 github.com/scala-incubator/autoproxy-plugin/wiki 但我不确定它的当前状态。 你可以期待一个完全基于宏的版本可以用于 2.11 版本,希望我能及时为 RC1 提供它【参考方案2】:

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

  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()

【讨论】:

抱歉,这与问题无关。【参考方案3】:

您尝试执行的操作称为记录连接,这是 Scala 的类型系统不支持的。 (Fwiw,存在提供此功能的类型系统 - 例如 this 和 this。)

我认为类型类可能适合您的用例,但我无法确定,因为该问题没有提供有关您要解决的问题的足够信息。

【讨论】:

实际上,you can encode extensible records 在 Scala 的类型系统中,但恐怕这不会直接帮助回答这个问题。 是的,我知道这一点。我们之前谈过。 嗯,好的,但你在回答中说的完全相反? 这样的记录可以在Scala中编码。这与他们是语言中的一流构造不同。 全面概括?不,但你可以例如连接元组。【参考方案4】:

这可以通过宏来完成(自 2.10.0-M3 起,它正式成为 Scala 的一部分)。 Here's a gist example of what you are looking for.

1) 我的宏生成一个本地类,它继承自提供的案例类和持久化,就像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 的方法有很多种,请参阅 "Metaprogramming in Scala 2.10" 了解更多信息。

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

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

【讨论】:

虽然这些东西非常有趣,但它也很硬核。我将如何将这几个编译阶段与 Maven 集成?当版本发布时,它会变得更加平易近人吗? 我认为它可以毫无问题地与 maven 一起使用,您可以发送编译器参数:scala-tools.org/mvnsites/maven-scala-plugin/…。我有一个基于 expectify 的 gradle 工作示例:github.com/flatMapDuke/TestMacro/commit/…。树的操作给我留下了深刻的印象,但它很棒:) 我同意,单独编译不方便。我会看看我能做些什么。【参考方案5】:

更新

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


以下解决方案基于 Scala 2.10.0-M3 反射 API 和 Scala Interpreter。它动态创建和缓存从原始案例类继承的类,并混合了特征。由于最大程度地缓存,该解决方案应该为每个原始案例类动态创建一个类,并在以后重用它。

由于新的反射 API 没有太多公开,也不是很稳定,也没有关于它的教程,但这个解决方案可能涉及一些愚蠢的重复动作和怪癖。

以下代码使用 Scala 2.10.0-M3 进行了测试。

1。持久化.scala

要混入的特征。请注意,由于我的程序更新,我对其进行了一些更改

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。沙盒.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)


【讨论】:

如果您对宏不满意,您可以使用新的工具箱 API,它可以让您编译 AST,并保证与解释器不同的是向后兼容。您可以复制/粘贴我的树操作代码,然后使用 scala.reflect.mirror.mkToolBox().runExpr(...) 编译并运行它。 另外,黑魔法到底是什么感觉?是否只是与构建工具集成的必要性,还是其他原因? @EugeneBurmako 非常感谢您对工具箱的建议,我一定会检查一下!关于黑魔法。没有像样的教程或文档,很难掌握正在发生的事情。此外,用于构建 AST 的 API 看起来像是为了造成痛苦的特定目的而开发的,尽管我也不能对反射 API 说得更好,但我知道这一切都是由与编译器世界的混合造成的。在此之上必须手动管理编译过程太多了,我最好不要解决这个问题。

以上是关于动态混合特征的主要内容,如果未能解决你的问题,请参考以下文章

hive 动态分区与混合分区

具有混合静态/动态内容的 UITableViewController 中的 NSRangeException

在动态 iframe 中注入表单时 IE 中的混合内容问题

SDL2+OpenGL 混合图像产生动态效果

Scala中的动态混合 - 有可能吗?

动态规划/背包问题背包问题第一阶段最终章:混合背包问题