Динамическое смешивание черт

Имея черту

trait Persisted {
  def id: Long
}

как реализовать метод, который принимает экземпляр любого класса case и возвращает его копию со смешанной чертой?

Сигнатура метода выглядит так:

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

person Nikita Volkov    schedule 29.04.2012    source источник
comment
Это интересный вопрос, но, рискуя заявить очевидное, почему ваши классы case не расширяют общую черту, которая предоставляет идентификатор?   -  person virtualeyes    schedule 30.04.2012
comment
@virtualeyes Это проблема очень тонко настроенного API ORM, над которым я работаю. Пока эти объекты не реализуют эту черту, они являются просто объектами бизнес-логики без ссылки на db, но этот подход открывает потребность в методе API, таком как def save[T](data: T): T with Persisted, который будет полагаться на метод, описанный в вопросе.   -  person Nikita Volkov    schedule 30.04.2012
comment
хорошо, у вас есть свои причины, но все ответы на сегодняшний день указывают на то, что в Scala вам, возможно, придется переосмыслить свой подход. С каким ORM вы работаете, с которым вы написали или сторонним?   -  person virtualeyes    schedule 30.04.2012
comment
@virtualeyes Я работаю над новым проектом ORM. Я не думаю, что это невозможно, я просто думаю, что это будет сложно, возможно, придется возиться с байт-кодом. Как только появится решение, я опубликую его или выберу здесь. Эмиль Х. сделал хорошее предложение, я постараюсь развить его.   -  person Nikita Volkov    schedule 30.04.2012
comment
аааааааааааааааааааааац в Невероятный ;-) С подходом Эмиля Х., как бы вы во время компиляции сделали новый T с Persisted? Похоже, вам понадобится оператор массового совпадения {} (т.е. вручную указать целевой класс), а затем, если это так, почему бы в этот момент просто не предоставить идентификатор? Хе-хе, ты поймешь это или сдашься и перейдешь на ScalaQuery ;-)   -  person virtualeyes    schedule 30.04.2012
comment
Забавно, что все мы, использующие Slick, сталкиваемся с одними и теми же проблемами, а typeafe не реагирует на их академическое мышление, предлагая готовое решение.   -  person SkyWalker    schedule 10.12.2016


Ответы (5)


Это можно сделать с помощью макросов (которые официально являются частью Scala с 2.10.0-M3). Вот краткий пример того, что вы ищете.

1) Мой макрос генерирует локальный класс, который наследуется от предоставленного класса case и 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) Расширения макросов можно проверить на работоспособность, если вы предоставите -Ymacro-debug-lite в качестве аргумента для scalac. В этом случае все расширения будут распечатаны, и вы сможете быстрее обнаруживать ошибки кодогенерации.

редактировать. Обновлен пример для 2.10.0-M7

person Eugene Burmako    schedule 30.04.2012

Невозможно добиться желаемого с помощью 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

См. Динамический миксин в Scala - возможно ли это? для получения дополнительной информации.

person Emil H    schedule 29.04.2012
comment
@NikitaVolkov Вы также можете взглянуть на Autoproxy. github.com/scala-incubator/autoproxy-plugin/wiki, но я м не уверен в текущем состоянии. - person Emil H; 01.05.2012
comment
Вы можете ожидать, что версия, полностью основанная на макросах, будет доступна для выпуска 2.11, надеюсь, она будет у меня к моменту выхода RC1. - person Kevin Wright; 16.02.2014

То, что вы пытаетесь сделать, называется конкатенацией записей, что не поддерживается системой типов Scala. (Fwiw, существуют системы типов, такие как this и this, которые предоставляют эту функцию.)

Я думаю, что классы типов могут соответствовать вашему варианту использования, но я не могу сказать наверняка, поскольку вопрос не дает достаточной информации о том, какую проблему вы пытаетесь решить.

person missingfaktor    schedule 29.04.2012
comment
Фактически, можно кодировать расширяемый записи в системе типов Scala, но я боюсь, что это не поможет напрямую ответить на этот вопрос. - person Miles Sabin; 29.04.2012
comment
Да, я в курсе. Мы говорили об этом раньше. - person missingfaktor; 29.04.2012
comment
Ладно, но в своем ответе вы сказали прямо противоположное? - person Miles Sabin; 29.04.2012
comment
Такие записи можно закодировать в Scala. Это отличается от того, что они являются конструкцией первого класса в языке. - person missingfaktor; 29.04.2012
comment
Я имел в виду, что в Scala нельзя объединять классы / черты. (Я думал, это очевидно из контекста?) - person missingfaktor; 29.04.2012
comment
В полной общности? Нет. Но вы можете, например, объединить кортежи. - person Miles Sabin; 29.04.2012

Обновлять

Вы можете найти актуальное рабочее решение, которое использует API Toolboxes для Scala 2.10.0-RC1 как часть SORM.


Следующее решение основано на API отражения Scala 2.10.0-M3 и интерпретаторе Scala. Он динамически создает и кэширует классы, унаследованные от исходных классов case со смешанными признаками. Благодаря максимальному кэшированию это решение должно динамически создавать только один класс для каждого исходного класса case и повторно использовать его позже.

Поскольку новый API отражения не так много раскрыт, и он не является стабильным, и по нему нет руководств, это решение может включать в себя некоторые глупые повторяющиеся действия и причуды.

Следующий код был протестирован на Scala 2.10.0-M3.

1. Persisted.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. 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)

}
person Nikita Volkov    schedule 01.05.2012
comment
Если вам неудобно работать с макросами, вы можете использовать новый API наборов инструментов, который позволяет компилировать AST и, в отличие от интерпретатора, гарантированно имеет обратную совместимость. Вы можете скопировать / вставить мой код манипуляции с деревом, а затем использовать scala.reflect.mirror.mkToolBox (). RunExpr (...) для его компиляции и запуска. - person Eugene Burmako; 01.05.2012
comment
Кроме того, что такое черная магия? Это просто необходимость интеграции с инструментами сборки или что-то еще? - person Eugene Burmako; 01.05.2012
comment
@EugeneBurmako Большое спасибо за предложение по поводу ящиков для инструментов, я обязательно его проверю! О черной магии. Дело в том, что без достойных руководств или документации очень сложно понять, что происходит. Кроме того, похоже, что API для создания AST разрабатывается с определенной целью - причинить боль, хотя я тоже не могу сказать ничего лучше об API отражения, но я понимаю, что все это вызвано смешением с миром компиляторов. Необходимость вручную управлять процессом компиляции - это слишком много, я лучше оставлю эту проблему нерешенной. - person Nikita Volkov; 01.05.2012

Хотя невозможно составить объект ПОСЛЕ его создания, вы можете провести очень широкие тесты, чтобы определить, имеет ли объект конкретную композицию, используя псевдонимы типов и структуры определения:

  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()
person Nthalk    schedule 29.04.2012
comment
Извините, но это не имеет отношения к вопросу. - person Nikita Volkov; 29.04.2012