开发者

Dynamic mixin in Scala - is it possible?

开发者 https://www.devze.com 2023-01-07 01:39 出处:网络
What I\'d like to achieve is having a proper implementation for def dynamix[A, B](a: A): A with B I may know what B is, but don\'t know what A is (but if B has a self type then I could add some con

What I'd like to achieve is having a proper implementation for

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

I may know what B is, but don't know what A is (but if B has a self type then I could add some constraints on A). The scala compiler is happy with the above signature, but I could not yet figure out how the implementation would look like - if it is possible at all.

Some options that came to my mind:

  • Using reflection/dynamic proxy.
    • Simplest case: A is an interface on Java level + I can instantiate B and it has no self type. I guess it would not be too hard (unless I run into some nasty, unexpected problems):

      create 开发者_如何学编程a new B (b), and also a proxy implementing both A and B and using an invocation handler delegating to either a or b.

    • If B can not be instantiated I could still create a subclass of it, and do as it was described above. If it also has a self type I would probably need some delegation here and there, but it may still work.
    • But what if A is a concrete type and I can't find a proper interface for it?
    • Would I run into more problems (e.g. something related to linearization, or special constructs helping Java interoperability)?
  • Using a kind of wrapping instead of a mixin and return B[A], a is accessible from b.

    Unfortunately in this case the caller would need to know how the nesting is done, which could be quite inconvenient if the mixing in/wrapping is done several times (D[C[B[A]]]) as it would need to find the right level of nesting to access the needed functionality, so I don't consider it a solution.

  • Implementing a compiler plugin. I have zero experience with it but my gut feeling is that it would not be trivial. I think Kevin Wright's autoproxy plugin has a bit similar goal, but it would not be enough for my problem (yet?).

Do you have any other ideas that might work? Which way would you recommend? What kind of "challenges" to expect?

Or should I forget it, because it is not possible with the current Scala constraints?

Intention behind my problem: Say I have a business workflow, but it's not too strict. Some steps have fixed order, but others do not, but at the end all of them has to be done (or some of them required for further processing).

A bit more concrete example: I have an A, I can add B and C to it. I don't care which is done first, but at the end I'll need an A with B with C.

Comment: I don't know too much about Groovy but SO popped up this question and I guess it's more or less the same as what I'd like, at least conceptional.


I believe this is impossible to do strictly at runtime, because traits are mixed in at compile-time into new Java classes. If you mix a trait with an existing class anonymously you can see, looking at the classfiles and using javap, that an anonymous, name-mangled class is created by 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 returns

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

While javap Main\$\$anon\$1 returns

Compiled from "mixin.scala"

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

As you can see, scalac creates a new anonymous class that is loaded at runtime; presumably the method eggs in this anonymous class creates an instance of Spam$class and calls eggs on it, but I'm not completely sure.

However, we can do a pretty hacky trick here:

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

Since you need to use the Scala compiler, AFAIK, this is probably close to the cleanest solution you could do to get this. It's quite slow, but memoization would probably help greatly.

This approach is pretty ridiculous, hacky, and goes against the grain of the language. I imagine all sorts of weirdo bugs could creep in; people who have used Java longer than me warn of the insanity that comes with messing around with classloaders.


I wanted to be able to construct Scala beans in my Spring application context, but I also wanted to be able to specify the mixins to be included in the constructed 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>

The difficulty is that Class.forName function does not allow me to specify the mixins. In the end, I extended the above hacky solution to Scala 2.9.1. So, here it is in its full gory; including bits of Spring.

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

}

The code cannot yet deal with constructors with parameters and does not copy annotations from the parent class’s constructor (should it do that?). However, it gives us a good starting point that is usable in the scala Spring namespace. Of course, don’t just take my word for it, verify it in a Specs2 specification:

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
  }

}
0

精彩评论

暂无评论...
验证码 换一张
取 消