Is it possible to write a scala macro whose return

2019-04-16 21:00发布

问题:

For a DSL I would like to be able to do something like:

object Creator { 
   def create[T](s :String) :Foo[T] = macro createImpl[T]
   def createImpl[T](c :Context)(s :c.Expr[String]) : c.Expr[Foo[T]] = {
        reify(new Foo[Any]())
   }
 }

My problem is to replace the Any in reify with something that will return the correctly parametrized version.

(above I use a string argument, but in the final version I plan to use the companion object of class T as a marker to know the argument-type of a Function1[T,Unit])

回答1:

You need to: a) write new Foo[T]() instead of new Foo[Any]() (easy) b) pass in the macro a representation of type T, namely, a value of type AbsTypeTag[T], by declaring the parameter T using a context bound: [T: c.AbsTypeTag].

Here's code which I tested in Scala 2.10.0-M7. Edit. In 2.10.0-RC1 AbsTypeTag has been renamed to WeakTypeTag. Everything else about macros and type tags remains the same.

Creator.scala:

import language.experimental.macros
import reflect.macros.Context
class Foo[T]
object Creator {
  def create[T](s: String): Foo[T] = macro createImpl[T]
  def createImpl[T: c.AbsTypeTag](c: Context)(s: c.Expr[String]): c.Expr[Foo[T]] = {
    import c.universe._
    reify(new Foo[T]())
  }
}

MacroClient.scala:

object Main extends App {
  println (Creator.create[Int](""))
}

Note that if you omit the type parameter, you will get a weird error:

scala> Creator.create[Int]("")
res2: Foo[Int] = Foo@4888884e

scala> Creator.create("")
<console>:12: error: macro has not been expanded
              Creator.create("")
                      ^

You also write:

(above I use a string argument, but in the final version I plan to use the companion object of class T as a marker to know the argument-type of a Function1[T,Unit])

but if I get it right, this sounds like a bad idea. Instead of writing Creator.create[T](otherArgs), the call syntax would be something like Creator.create(T, otherArgs), not a big advantage (if any). But you can't even get the latter syntax: if class A and object A are companions, their types are not related: the first has type A, the second has type A.type where A is the companion object and not the type of class A.


Update: how to get the Creator create Foo syntax to work and return an instance of Foo, if you have control over Foo. Since you ask about the Any type argument to reify, I assume you are asking about the type argument. That makes only sense if you want the static return type of Creator.create to be T and not Any; otherwise, you should clarify your question.

The problem here has little to do with macros. Creator create Foo passes the object Foo to Creator.create, whose declaration needs to express, given Foo.type, the type Foo through a type expression. Type expressions in Scala are quite limited - they can't use reflection, for instance. But given a type, they can select its type members.

trait Companion[Class]
//How to declare a companion
class Foo
object Foo extends Companion[Foo]
/*I'm cheating: an implementation of Companion does not need to be a true Companion. You can add documentation to explain how Companion is supposed to be used. */
object Bar extends Companion[Foo]
//But this is also useful - you can't create your own companion objects for pre-existing types, but you can still create the right instances of Companion:
object pInt extends Companion[Int]
object Creator {
  //T with Companion[S] is needed to workaround a type inference bug (SI-5298) and allow S to be correctly inferred.
  def create[S, T <: Companion[S]](obj: T with Companion[S]): S = ???
}

This is limited since you need to alter the companion object, but I'm pretty sure you can't do better. I know no way, in a type expression (what you can use in place of S when declaring the return type of create) of getting from a companion object to its associated class type in general, and I don't think there's one.

Now, changing the above to use macros is straightforward:

import language.experimental.macros
import reflect.macros.Context
class Foo[T]
object Creator {
  //T with Companion[S] is needed to workaround a type inference bug (SI-5298) and allow S to be correctly inferred.
  def create[S, T <: Companion[S]](obj: T with Companion[S]): Foo[S] = macro createImpl[S, T]
  def createImpl[S: c.AbsTypeTag, T <: Companion[S]: c.AbsTypeTag](c: Context)(obj: c.Expr[T with Companion[S]]): c.Expr[Foo[S]] = {
    import c.universe._
    reify(new Foo[S]())
  }
}