Динамический миксин в Scala - это возможно?
Чего я хотел бы добиться, так это иметь правильную реализацию
def dynamix[A, B](a: A): A with B
Я могу знать, что такое B, но не знаю, что такое A (но если у B есть собственный тип, я мог бы добавить некоторые ограничения на A). Компилятор scala доволен приведенной выше сигнатурой, но я пока не могу понять, как будет выглядеть реализация - если это вообще возможно.
Несколько вариантов, которые пришли мне в голову:
- Использование отражения / динамического прокси.
- Простейший случай: A - это интерфейс на уровне Java + я могу создать экземпляр B, и он не имеет собственного типа. Я думаю, это не будет слишком сложно (если я не столкнусь с некоторыми неприятными, неожиданными проблемами):
создайте новый B (b), а также прокси-сервер, реализующий как A, так и B и использующий обработчик вызова, делегирующий либо a, либо b. - Если B не может быть создан, я все равно мог бы создать его подкласс и делать, как было описано выше. Если у него также есть собственный тип, мне, вероятно, понадобится некоторое делегирование здесь и там, но оно все еще может работать.
- Но что, если A - конкретный тип, и я не могу найти подходящий интерфейс для него?
- Могу ли я столкнуться с большим количеством проблем (например, что-то, связанное с линеаризацией или специальными конструкциями, помогающими взаимодействию Java)?
- Простейший случай: A - это интерфейс на уровне Java + я могу создать экземпляр B, и он не имеет собственного типа. Я думаю, это не будет слишком сложно (если я не столкнусь с некоторыми неприятными, неожиданными проблемами):
- Используя своего рода обертку вместо mixin и возвращая B[A], a доступен из b.
К сожалению, в этом случае вызывающая сторона должна знать, как выполняется вложение, что может быть довольно неудобно, если смешивание / обертывание выполняется несколько раз (D[C[B[A]]])), так как ему нужно будет найти правильный уровень вложенности для доступа к необходимой функциональности, поэтому я не считаю это решением. - Реализация плагина компилятора. У меня нет опыта с этим, но мое внутреннее чувство состоит в том, что это не было бы тривиально. Я думаю, что плагин Кевина Райта имеет несколько схожую цель, но этого будет недостаточно для моей проблемы (пока?).
У вас есть другие идеи, которые могут сработать? Какой способ вы бы порекомендовали? Какие "вызовы" ожидать?
Или я должен забыть это, потому что это невозможно с текущими ограничениями Scala?
Намерение, стоящее за моей проблемой: скажем, у меня есть бизнес-процесс, но он не слишком строг. Некоторые шаги имеют фиксированный порядок, а другие нет, но в конце все они должны быть выполнены (или некоторые из них требуются для дальнейшей обработки).
Немного более конкретный пример: у меня есть A, я могу добавить B и C к нему. Мне все равно, что делать в первую очередь, но в конце мне понадобится A с B с C.
Комментарий: Я не знаю слишком много о Groovy, но ТАК поднял этот вопрос, и я думаю, что он более или менее совпадает с тем, что я хотел бы, по крайней мере, концептуально.
2 ответа
Я считаю, что это невозможно сделать строго во время выполнения, потому что во время компиляции черты смешиваются с новыми классами Java. Если вы смешиваете черту с существующим классом анонимно, вы можете увидеть, просматривая файлы классов и используя javap, анонимный, искаженный по имени класс создается с помощью scalac:
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 дольше, чем я, предупреждают о безумии, связанном с вознями с загрузчиками классов.
Я хотел иметь возможность создавать 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)
}
}
Код еще не может иметь дело с конструкторами с параметрами и не копирует аннотации из конструктора родительского класса (должен ли он это делать?). Тем не менее, это дает нам хорошую отправную точку, которую можно использовать в пространстве имен Scala Spring. Конечно, не просто поверьте мне на слово, проверьте это в спецификации 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
}
}