Last active
December 4, 2018 03:29
-
-
Save xeno-by/8b64ca6a73775147941e to your computer and use it in GitHub Desktop.
Towards perfect compile-time proxies in Scala
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
// PROBLEM STATEMENT | |
// Let's build Proxy[T], an object that can capture calls to methods of an underlying object. | |
// We want the proxy to be type-safe, i.e. we need to verify that the names of the calls | |
// and the arguments that are passed to those calls are well-typed wrt the type of the proxee. | |
object Test extends App { | |
trait Foo { /* ... */ } | |
val proxy: Proxy[Foo] = ??? | |
proxy.bar() | |
proxy.bar[Int](1) | |
proxy.bar[Int, Int](1, 2) | |
} | |
// STEP 1: A NAIVE APPROACH THAT DOESN'T WORK | |
// We need a proxy, so let's use Dynamic, which was seemingly built exactly for that. | |
// Since we need to proxy method calls, let's use applyDynamic. | |
// | |
// Scala has term-level varargs, but it doesn't have type-level varargs. | |
// Therefore we need to use overloading on applyDynamic or | |
// otherwise we won't be able to proxy polymorphic methods. | |
// | |
// However, if we try to do something like the code below we will end up | |
// with a compilation error, because the overloads are going to have identical erasures. | |
class ProxyAttempt1[T](x: T) extends Dynamic { | |
def applyDynamic(name: String)(xs: Any*): Any = macro ??? | |
def applyDynamic[T1](name: String)(xs: Any*): Any = macro ??? | |
def applyDynamic[T1, T2](name: String)(xs: Any*): Any = macro ??? | |
} | |
// STEP 2: A MORE SOPHISTICATED ATTEMPT THAT STILL DOESN'T WORK | |
// Here's an interesting trick that comes from the land of scala-virtualized and LMS. | |
// We can have different dummy implicit parameters on every overload, | |
// with corresponding implicit values always in scope. | |
// This will make erased signatures of those overloads different, | |
// and will also be completely invisible for the users. | |
// | |
// Unfortunately, this doesn't work because of a bug in the applyDynamic desugarer. | |
// When the typer sees `foo.bar[Baz](args)`, it desugars it into `foo.applyDynamic[Baz]("bar")[Baz](args)`. | |
// This second `[Baz]` type application really messes things up, making it impossible to use implicits. | |
trait OverloadHack0 | |
object OverloadHack0 { implicit val ev: OverloadHack0 = new OverloadHack0 } | |
trait OverloadHack1 | |
object OverloadHack1 { implicit val ev: OverloadHack1 = new OverloadHack1 } | |
trait OverloadHack2 | |
object OverloadHack2 { implicit val ev: OverloadHack2 = new OverloadHack2 } | |
class ProxyAttempt2[T](x: T) extends Dynamic { | |
def applyDynamic(name: String)(xs: Any*)(implicit ev: OverloadHack0): Any = macro ??? | |
def applyDynamic[T1](name: String)(xs: Any*)(implicit ev: OverloadHack1): Any = macro ??? | |
def applyDynamic[T1, T2](name: String)(xs: Any*)(implicit ev: OverloadHack2): Any = macro ??? | |
} | |
// STEP 3: WORKING AROUND THE APPLYDYNAMIC DESUGARING BUG | |
// We can hack around the double type application bug by splitting the `(xs: Any*)` part | |
// into an `apply` method on some object that's going to be returned from `applyDynamic`. | |
// This makes it possible to accommodate two type applications, but it still doesn't | |
// solve issue with erasure. | |
// | |
// Luckily, we can again use implicits to combat erasure. Now let's have the `name` parameter | |
// to be of some weird type that we can convert strings to. That will again ensure | |
// smooth user experience while providing distinct erasures. | |
// | |
// We're really close, but unfortunately there's a problem (and I considered it to be fatal for a few years). | |
// When you call a method with no type arguments (e.g. `proxy.bar()`), the desugaring into applyDynamic | |
// is going to fail typechecking, because the typer won't be able to pick the correct overload. | |
// Overloads with type parameters are going to match as well, because those type parameters can be inferred! | |
class Cont0 { def apply(xs: Any*): Any = macro ??? } | |
class Cont1 { def apply[T1](xs: Any*): Any = macro ??? } | |
class Cont2 { def apply[T1, T2](xs: Any*): Any = macro ??? } | |
class ProxyAttempt3[T](x: T) extends Dynamic { | |
def applyDynamic(name: AnotherHack0): Cont0 = ??? | |
def applyDynamic[T1](name: AnotherHack1): Cont1 = ??? | |
def applyDynamic[T1, T2](name: AnotherHack2): Cont2 = ??? | |
} | |
trait AnotherHack0 | |
object AnotherHack0 { implicit def hack0(s: String): AnotherHack0 = ??? } | |
trait AnotherHack1 | |
object AnotherHack1 { implicit def hack1(s: String): AnotherHack1 = ??? } | |
trait AnotherHack2 | |
object AnotherHack2 { implicit def hack2(s: String): AnotherHack2 = ??? } | |
// STEP 4: MACROS TO THE RESCUE | |
// Allright, so we need to customize the behavior of the typechecker. What do we do? | |
// Well, let's try using macros! | |
// | |
// My first attempt was turning hack0, hack1 and hack2 into macros and | |
// having hack1 and hack2 explode if they are called for a proxied call | |
// which doesn't have type arguments. Unfortunately, this didn't work, because | |
// overload resolution only looks up suitable implicit conversions, | |
// but it doesn't expand them if they are macros. | |
// | |
// When a macro doesn't get the job done, you write another macro. | |
// We can't force the typer to expand the implicit conversion if it's a macro, | |
// but we can have it typecheck the invocation of that implicit conversion | |
// and, thanks to the fix of SI-3346, that typecheck can involve typechecking | |
// implicit parameters of that implicit conversion, and those can be macros!! | |
// | |
// Inside those macros, we can check whether the call that's undergoing the applyDynamic transformation | |
// has type arguments or not and then we can deny harmful overloads of applyDynamic. | |
// See an example (very dirty) implementation of such a gatekeeper below. | |
// Executable code for everything can be found at https://gist.github.com/xeno-by/840329c34d82da906724. | |
trait FinalHack0 | |
object FinalHack0 { implicit def hack0(s: String)(implicit hackhack0: Gatekeeper0): FinalHack0 = ??? } | |
trait FinalHack1 | |
object FinalHack1 { implicit def hack1(s: String)(implicit hackhack1: Gatekeeper1): FinalHack1 = ??? } | |
trait FinalHack2 | |
object FinalHack2 { implicit def hack2(s: String)(implicit hackhack2: Gatekeeper2): FinalHack2 = ??? } | |
class ProxyAttempt4[T](x: T) extends Dynamic { | |
def applyDynamic(name: FinalHack0): Cont0 = ??? | |
def applyDynamic[T1](name: FinalHack1): Cont1 = ??? | |
def applyDynamic[T1, T2](name: FinalHack2): Cont2 = ??? | |
} | |
trait Gatekeeper0 | |
object Gatekeeper0 { implicit def materialize: Gatekeeper0 = macro Macros.gatekeeper } | |
trait Gatekeeper1 | |
object Gatekeeper1 { implicit def materialize: Gatekeeper1 = macro Macros.gatekeeper } | |
trait Gatekeeper2 | |
object Gatekeeper2 { implicit def materialize: Gatekeeper2 = macro Macros.gatekeeper } | |
class Macros(val c: Context) { | |
import c.universe._ | |
def gatekeeper: Tree = { | |
val powerc = c.asInstanceOf[scala.reflect.macros.contexts.Context] | |
val Apply(target, args) = powerc.callsiteTyper.context.tree.asInstanceOf[Tree] | |
val hasTargs = target match { case _: TypeApply => true; case _ => false } | |
if (!hasTargs && !c.macroApplication.toString.startsWith("Gatekeeper0")) c.abort(c.enclosingPosition, "denied") | |
q"???" | |
} | |
} | |
// STEP 5: FUTURE WORK | |
// This still doesn't handle the case of multiple argument lists, | |
// because `applyDynamic` only processes one argument list at a time. | |
// Nevertheless, I believe that this step should be more or less straightforward. | |
// Unfortunately, I don't have time to elaborate, so let's leave it for future work. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
If you have comments to this gist, please don't reply here, because I most likely won't notice your replies. Consider letting me know at https://twitter.com/xeno_by/status/555051365693915137.