Динамический миксин в Scala - это возможно? - PullRequest
40 голосов
/ 15 июля 2010

Чего я хотел бы добиться, так это иметь правильную реализацию для

def dynamix[A, B](a: A): A with B

. Я могу знать, что такое B, но не знаю, что такое A (но если у B есть собственный тип, тогда ямог бы добавить некоторые ограничения на А).Компилятор scala доволен вышеуказанной сигнатурой, но я пока не могу понять, как будет выглядеть реализация - если это вообще возможно.

Некоторые варианты, которые мне приходили в голову:

  • Использование рефлексии / динамического прокси.
    • Простейший случай: A - это интерфейс на уровне Java + я могу создать экземпляр B, и он не имеет собственного типа.Я предполагаю, что это не будет слишком сложно (если я не столкнусь с некоторыми неприятными, неожиданными проблемами):
      создайте новый B (b), а также прокси, реализующий и A и B, и использующий обработчик вызова, делегирующий либо a, либоб.
    • Если B не может быть создан, я все равно мог бы создать его подкласс и сделать, как было описано выше.Если он также имеет собственный тип, мне, вероятно, понадобится некоторое делегирование здесь и там, но оно все еще может работать.
    • Но что, если A - это конкретный тип, и я не могу найти подходящий интерфейс для него?
    • Могу ли я столкнуться с большим количеством проблем (например, что-то, связанное с линеаризацией или специальными конструкциями, помогающими взаимодействию Java)?
  • Использование вида обертки вместо mixin и возврата B [A], a доступно из b.
    К сожалению, в этом случае вызывающей стороне необходимо знать, как вложениясделано, что может быть довольно неудобно, если смешивание / обертывание выполняется несколько раз (D [C [B [A]]]), так как для доступа к необходимой функциональности потребуется найти правильный уровень вложенности, поэтому я нене считаю это решением.
  • Реализация плагина компилятора.У меня нет опыта с этим, но мое внутреннее чувство состоит в том, что это не было бы тривиально.Я думаю, что плагин autoproxy Кевина Райта имеет немного похожую цель, но этого не достаточно для моей проблемы (пока?).

Есть ли у вас какие-либо другие идеи, которые могли бы работать?Какой способ вы бы порекомендовали?Каких «вызовов» ожидать?
Или я должен забыть об этом, потому что это невозможно с текущими ограничениями Scala?

Намерение, стоящее за моей проблемой: скажем, у меня есть бизнес-процесс, но это не такслишком строг.Некоторые шаги имеют фиксированный порядок, а другие нет, но в конце все они должны быть выполнены (или некоторые из них требуются для дальнейшей обработки).
Немного более конкретный пример: у меня есть A, я могу добавитьB и C к нему.Мне все равно, что делать в первую очередь, но в конце мне понадобится буква A с буквой B с буквой C.

Комментарий: я не знаю слишком много о Groovy, но ТАК выскочил этовопрос и я думаю, что это более или менее так же, как я хотел бы, по крайней мере, концептуально.

Ответы [ 2 ]

26 голосов
/ 17 июля 2010

Я считаю, что это невозможно сделать строго во время выполнения, потому что во время компиляции черты смешиваются с новыми классами Java. Если вы смешаете черту с существующим классом анонимно, вы увидите, просматривая файлы классов и используя javap, что скалярное имя создает анонимный, искаженный по имени класс:

class Foo {
  def bar = 5
}

trait Spam {
  def eggs = 10
}

object Main {
  def main(args: Array[String]) = {
    println((new Foo with Spam).eggs)
  }
}

scalac Mixin.scala; ls *.class возвращает

Foo.class Main$.class Spam$class.class Main$$anon$1.class Main.class Spam.class

Пока javap Main\$\$anon\$1 возвращает

Compiled from "mixin.scala"

public final class Main$$anon$1 extends Foo implements Spam{
    public int eggs();
    public Main$$anon$1();
}

Как видите, scalac создает новый анонимный класс, который загружается во время выполнения; предположительно, метод eggs в этом анонимном классе создает экземпляр Spam$class и вызывает для него eggs, но я не совсем уверен.

Однако , мы можем сделать довольно хакерский трюк здесь:

import scala.tools.nsc._;
import scala.reflect.Manifest

object DynamicClassLoader {
  private var id = 0
  def uniqueId = synchronized {  id += 1; "Klass" + id.toString }
}

class DynamicClassLoader extends 
    java.lang.ClassLoader(getClass.getClassLoader) {
  def buildClass[T, V](implicit t: Manifest[T], v: Manifest[V]) = {

    // Create a unique ID
    val id = DynamicClassLoader.uniqueId

    // what's the Scala code we need to generate this class?
    val classDef = "class %s extends %s with %s".
      format(id, t.toString, v.toString)

    println(classDef)

    // fire up a new Scala interpreter/compiler
    val settings = new Settings(null)
    val interpreter = new Interpreter(settings)

    // define this class
    interpreter.compileAndSaveRun("<anon>", classDef)

    // get the bytecode for this new class
    val bytes = interpreter.classLoader.getBytesForClass(id)

    // define the bytecode using this classloader; cast it to what we expect
    defineClass(id, bytes, 0, bytes.length).asInstanceOf[Class[T with V]]
  }

}


val loader = new DynamicClassLoader

val instance = loader.buildClass[Foo, Spam].newInstance
instance.bar
// Int = 5
instance.eggs
// Int = 10

Поскольку необходимо для использования компилятора Scala, AFAIK, это, вероятно, близко к самому чистому решению, которое вы могли бы сделать, чтобы получить это. Это довольно медленно, но запоминание, вероятно, очень поможет.

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

3 голосов
/ 23 января 2012

Я хотел иметь возможность создавать bean-компоненты Scala в контексте моего приложения Spring, но я также хотел иметь возможность указать миксины, которые должны быть включены в созданный bean-компонент:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:context="http://www.springframework.org/schema/context"
  xmlns:scala="http://www.springframework.org/schema/scala"
  xsi:schemaLocation=...>

  <scala:bean class="org.cakesolutions.scala.services.UserService" >
    <scala:with trait="org.cakesolutions.scala.services.Mixin1" />
    <scala:with trait="org.cakesolutions.scala.services.Mixin2" />

    <scala:property name="dependency" value="Injected" />
  <scala:bean>
</beans>

Сложность в том, что функция Class.forName не позволяет мне указывать миксины. В конце концов, я расширил вышеупомянутое хакерское решение на Scala 2.9.1. Итак, здесь все в полном порядке; включая биты весны.

class ScalaBeanFactory(private val beanType: Class[_ <: AnyRef],
                       private val mixinTypes: Seq[Class[_ <: AnyRef]]) {
  val loader = new DynamicClassLoader
  val clazz = loader.buildClass(beanType, mixinTypes)

   def getTypedObject[T] = getObject.asInstanceOf[T]

   def getObject = {
     clazz.newInstance()
   }

   def getObjectType = null
   def isSingleton = true

object DynamicClassLoader {
  private var id = 0
  def uniqueId = synchronized {  id += 1; "Klass" + id.toString }
}

class DynamicClassLoader extends java.lang.ClassLoader(getClass.getClassLoader) {

  def buildClass(t: Class[_ <: AnyRef], vs: Seq[Class[_ <: AnyRef]]) = {
    val id = DynamicClassLoader.uniqueId

    val classDef = new StringBuilder

    classDef.append("class ").append(id)
    classDef.append(" extends ").append(t.getCanonicalName)
    vs.foreach(c => classDef.append(" with %s".format(c.getCanonicalName)))

    val settings = new Settings(null)
    settings.usejavacp.value = true
    val interpreter = new IMain(settings)


    interpreter.compileString(classDef.toString())


    val r = interpreter.classLoader.getResourceAsStream(id)
    val o = new ByteArrayOutputStream
    val b = new Array[Byte](16384)
    Stream.continually(r.read(b)).takeWhile(_ > 0).foreach(o.write(b, 0, _))
    val bytes = o.toByteArray

    defineClass(id, bytes, 0, bytes.length)
  }

}

Код пока не может иметь дело с конструкторами с параметрами и не копирует аннотации из конструктора родительского класса (должен ли он это делать?). Тем не менее, это дает нам хорошую отправную точку, которую можно использовать в пространстве имен Spring Scala. Конечно, не поверьте мне на слово, проверьте это в спецификации Specs2:

class ScalaBeanFactorySpec extends Specification {

  "getTypedObject mixes-in the specified traits" in {
    val f1 = new ScalaBeanFactory(classOf[Cat],
                                  Seq(classOf[Speaking], classOf[Eating]))

    val c1 = f1.getTypedObject[Cat with Eating with Speaking]

    c1.isInstanceOf[Cat with Eating with Speaking] must_==(true)

    c1.speak    // in trait Speaking
    c1.eat      // in trait Eating
    c1.meow     // in class Cat
  }

}
...