Created
November 3, 2012 19:28
-
-
Save xeno-by/4008389 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// for the story behind this snippet of code see: | |
// http://stackoverflow.com/questions/13185405/structural-subtyping-reflection | |
// requires Scala 2.10.0-RC1 or higher | |
// you also need to have scala-reflect.jar and scala-compiler.jar on your classpath | |
import scala.reflect.runtime.universe._ | |
object Test extends App { | |
val f = (r: {val s: String}) => r.s | |
// we want to build a function `mock` which: | |
// * takes `fn`, a T => R, with T being a structural type | |
// * takes `mocker`, a Symbol => Any, which produces a mock value for a given declaration | |
// * returns the result of invoking `fn` with an argument generated by `mocker` | |
// | |
// example: | |
// * `fn`: (r: {def s: String}) => r.s | |
// * `mocker`: sym => sym.name.toString | |
// * the result will be "s" | |
def mock[T: TypeTag, R](fn: T => R, mocker: Symbol => Any): R = { | |
// having annotated T with the TypeTag context bound | |
// we persist full information about its type to be available at runtime | |
// afterwards we pattern match against that type using the types declared in the reflection API | |
// info about the reflection API is available here: http://docs.scala-lang.org/overviews/reflection/overview.html | |
// this stuff required scala-reflect.jar, which means Scala 2.10.0-RC1 or higher | |
val RefinedType(parents, decls) = typeOf[T] | |
// we're going to generate a mock argument for `fn` at runtime using the reflection API | |
// to do that we first create a toolbox, i.e. a runtime Scala compiler | |
// this functionality requires scala-compiler.jar | |
// more information about toolboxes is available via the aforementioned link | |
import scala.tools.reflect.ToolBox | |
val tb = runtimeMirror(getClass.getClassLoader).mkToolBox() | |
// to generate a class with toolboxes, we need to create an abstract syntax tree for it | |
// see http://www.scala-lang.org/archives/downloads/distrib/files/nightly/docs/library/index.html#scala.reflect.api.Printers | |
// to understand how I figured out what exactly is the shape of the tree I need to build | |
// (for simplicity I assume that the structural type only contains nullary defs) | |
val emptyCtor = DefDef(NoMods, nme.CONSTRUCTOR, Nil, List(Nil), TypeTree(), Block(List(Apply(Select(Super(This(tpnme.EMPTY), tpnme.EMPTY), nme.CONSTRUCTOR), Nil)), Literal(Constant(())))) | |
val classDef = ClassDef(NoMods, newTypeName("Mock"), Nil, Template(parents map TypeTree, emptyValDef, emptyCtor +: decls.toList.map(decl => { | |
val NullaryMethodType(typeOfVal) = decl.typeSignature | |
// free terms allow to refer to runtime values in the generated code | |
// creation syntax isn't pretty, but it works | |
// though we'll most likely improve the syntax in the future | |
val mockValueOfVal = build.newFreeTerm(decl.name.toString, mocker(decl)) | |
build.setTypeSignature(mockValueOfVal, typeOfVal) | |
// a nasty caveat - if one doesn't provide any flags to the method creator | |
// (effectively leaving it public by default) | |
// then the compiler somehow decides to make the method private (probably because this is a local class) | |
// which leads to NoSuchMethodExceptions later | |
// therefore we trick the compiler into not touching visibility by making the method protected | |
// these little things is what makes reflection experimental in 2.10.0 | |
DefDef(Modifiers(Flag.PROTECTED), decl.name.toTermName, Nil, Nil, TypeTree(typeOfVal), Ident(mockValueOfVal)) | |
}))) | |
// now we wrap the class definition into a snippet that instantiates it | |
// and then use the toolbox to compile and evaluate that snippet | |
val code = Block(List(classDef), Apply(Select(New(Ident(newTypeName("Mock"))), nme.CONSTRUCTOR), List())) | |
println(code) | |
val mockInstance = tb.eval(code).asInstanceOf[T] | |
// finally, the invocation | |
fn(mockInstance) | |
} | |
val fn = (r: {def s: String}) => r.s | |
val result = mock(fn, sym => sym.name.toString) | |
println(result) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment