Mixing in a trait dynamically
Having a trait
trait Persisted {
def id: Long
}
how do I implement a method that accepts an instance of any case class and returns its copy with the trait mixed in?
The signature of the method looks like:
def toPersisted[T](instance: T, id: Long): T with Persisted
Solution 1:
This can be done with macros (that are officially a part of Scala since 2.10.0-M3). Here's a gist example of what you are looking for.
1) My macro generates a local class that inherits from the provided case class and Persisted, much like new T with Persisted
would do. Then it caches its argument (to prevent multiple evaluations) and creates an instance of the created class.
2) How did I know what trees to generate? I have a simple app, parse.exe that prints the AST that results from parsing input code. So I just invoked parse class Person$Persisted1(first: String, last: String) extends Person(first, last) with Persisted
, noted the output and reproduced it in my macro. parse.exe is a wrapper for scalac -Xprint:parser -Yshow-trees -Ystop-after:parser
. There are different ways to explore ASTs, read more in "Metaprogramming in Scala 2.10".
3) Macro expansions can be sanity-checked if you provide -Ymacro-debug-lite
as an argument to scalac. In that case all expansions will be printed out, and you'll be able to detect codegen errors faster.
edit. Updated the example for 2.10.0-M7
Solution 2:
It is not possible to achieve what you want using vanilla scala. The problem is that the mixins like the following:
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
create a Foo with Bar
mixed in, but it is not done at runtime. The compiler simply generates a new anonymous class:
scala> fooWithBar.getClass
res3: java.lang.Class[_ <: Foo] = class $anon$1
See Dynamic mixin in Scala - is it possible? for more info.
Solution 3:
What you are trying to do is known as record concatenation, something that Scala's type system does not support. (Fwiw, there exist type systems - such as this and this - that provide this feature.)
I think type classes might fit your use case, but I cannot tell for sure as the question doesn't provide sufficient information on what problem you are trying to solve.
Solution 4:
Update
You can find an up to date working solution, which utilizes a Toolboxes API of Scala 2.10.0-RC1 as part of SORM project.
The following solution is based on the Scala 2.10.0-M3 reflection API and Scala Interpreter. It dynamically creates and caches classes inheriting from the original case classes with the trait mixed in. Thanks to caching at maximum this solution should dynamically create only one class for each original case class and reuse it later.
Since the new reflection API isn't that much disclosed nor is it stable and there are no tutorials on it yet this solution may involve some stupid repitative actions and quirks.
The following code was tested with Scala 2.10.0-M3.
1. Persisted.scala
The trait to be mixed in. Please note that I've changed it a bit due to updates in my program
trait Persisted {
def key: String
}
2. PersistedEnabler.scala
The actual worker object
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
The test app
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)
}