web-dev-qa-db-fra.com

Obtention d'un type structurel avec les méthodes d'une classe anonyme à partir d'une macro

Supposons que nous voulons écrire une macro qui définit une classe anonyme avec certains membres ou méthodes de type, puis crée une instance de cette classe qui est typée statiquement comme type structurel avec ces méthodes, etc. Cela est possible avec le système de macros en 2.10. 0, et la partie membre de type est extrêmement simple:

object MacroExample extends ReflectionUtils {
  import scala.language.experimental.macros
  import scala.reflect.macros.Context

  def foo(name: String): Any = macro foo_impl
  def foo_impl(c: Context)(name: c.Expr[String]) = {
    import c.universe._

    val Literal(Constant(lit: String)) = name.tree
    val anon = newTypeName(c.fresh)

    c.Expr(Block(
      ClassDef(
        Modifiers(Flag.FINAL), anon, Nil, Template(
          Nil, emptyValDef, List(
            constructor(c.universe),
            TypeDef(Modifiers(), newTypeName(lit), Nil, TypeTree(typeOf[Int]))
          )
        )
      ),
      Apply(Select(New(Ident(anon)), nme.CONSTRUCTOR), Nil)
    ))
  }
}

(Où ReflectionUtils est un trait de commodité qui fournit ma méthode constructor.)

Cette macro nous permet de spécifier le nom du membre de type de la classe anonyme sous la forme d'un littéral de chaîne:

scala> MacroExample.foo("T")
res0: AnyRef{type T = Int} = $1$$1@7da533f6

Notez qu'il est correctement saisi. Nous pouvons confirmer que tout fonctionne comme prévu:

scala> implicitly[res0.T =:= Int]
res1: =:=[res0.T,Int] = <function1>

Supposons maintenant que nous essayons de faire la même chose avec une méthode:

def bar(name: String): Any = macro bar_impl
def bar_impl(c: Context)(name: c.Expr[String]) = {
  import c.universe._

  val Literal(Constant(lit: String)) = name.tree
  val anon = newTypeName(c.fresh)

  c.Expr(Block(
    ClassDef(
      Modifiers(Flag.FINAL), anon, Nil, Template(
        Nil, emptyValDef, List(
          constructor(c.universe),
          DefDef(
            Modifiers(), newTermName(lit), Nil, Nil, TypeTree(),
            c.literal(42).tree
          )
        )
      )
    ),
    Apply(Select(New(Ident(anon)), nme.CONSTRUCTOR), Nil)
  ))
}

Mais lorsque nous l'essayons, nous n'obtenons pas de type structurel:

scala> MacroExample.bar("test")
res1: AnyRef = $1$$1@da12492

Mais si nous y collons une classe anonyme supplémentaire:

def baz(name: String): Any = macro baz_impl
def baz_impl(c: Context)(name: c.Expr[String]) = {
  import c.universe._

  val Literal(Constant(lit: String)) = name.tree
  val anon = newTypeName(c.fresh)
  val wrapper = newTypeName(c.fresh)

  c.Expr(Block(
    ClassDef(
      Modifiers(), anon, Nil, Template(
        Nil, emptyValDef, List(
          constructor(c.universe),
          DefDef(
            Modifiers(), newTermName(lit), Nil, Nil, TypeTree(),
            c.literal(42).tree
          )
        )
      )
    ),
    ClassDef(
      Modifiers(Flag.FINAL), wrapper, Nil,
      Template(Ident(anon) :: Nil, emptyValDef, constructor(c.universe) :: Nil)
    ),
    Apply(Select(New(Ident(wrapper)), nme.CONSTRUCTOR), Nil)
  ))
}

Ça marche:

scala> MacroExample.baz("test")
res0: AnyRef{def test: Int} = $2$$1@6663f834

scala> res0.test
res1: Int = 42

C'est extrêmement pratique - cela vous permet de faire des choses comme this , par exemple - mais je ne comprends pas pourquoi cela fonctionne, et la version membre de type fonctionne, mais pas bar. Je sais cela peut ne pas être un comportement défini , mais cela a-t-il un sens? Existe-t-il un moyen plus propre d'obtenir un type structurel (avec les méthodes) à partir d'une macro?

182
Travis Brown

Travis ici répond à cette question en double. Il existe des liens vers le problème dans le tracker et vers la discussion d'Eugene (dans les commentaires et la liste de diffusion).

Dans la célèbre section "Skylla et Charybdis" du vérificateur de type, notre héros décide de ce qui doit échapper à l'anonymat sombre et voir la lumière comme un membre du type structurel.

Il existe deux façons de tromper le vérificateur de type (qui n'implique pas le stratagème d'Ulysse consistant à serrer un mouton dans ses bras). Le plus simple est d'insérer une instruction factice afin que le bloc ne ressemble pas à une classe anonyme suivi de son instanciation.

Si le typer remarque que vous êtes un terme public qui n'est pas référencé par l'extérieur, cela vous rendra privé.

object Mac {
  import scala.language.experimental.macros
  import scala.reflect.macros.Context

  /* Make an instance of a structural type with the named member. */
  def bar(name: String): Any = macro bar_impl

  def bar_impl(c: Context)(name: c.Expr[String]) = {
    import c.universe._
    val anon = TypeName(c.freshName)
    // next week, val q"${s: String}" = name.tree
    val Literal(Constant(s: String)) = name.tree
    val A    = TermName(s)
    val dmmy = TermName(c.freshName)
    val tree = q"""
      class $anon {
        def $A(i: Int): Int = 2 * i
      }
      val $dmmy = 0
      new $anon
    """
      // other ploys
      //(new $anon).asInstanceOf[{ def $A(i: Int): Int }]
      // reference the member
      //val res = new $anon
      //val $dmmy = res.$A _
      //res
      // the canonical ploy
      //new $anon { }  // braces required
    c.Expr(tree)
  }
}
9
som-snytt