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

181

Supposons que nous voulions écrire une macro qui définit une classe anonyme avec des membres de type ou des méthodes, puis crée une instance de cette classe qui est typée statiquement comme un type structurel avec ces méthodes, etc. Ceci est possible avec le système de macros de la version 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ù ReflectionUtilsest un trait de commodité qui fournit ma constructorméthode.)

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

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 essayions 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 celle-ci , par exemple - mais je ne comprends pas pourquoi cela fonctionne, et la version du membre de type fonctionne, mais pas bar. Je sais que ce n'est peut-être pas 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 dessus) à partir d'une macro?

Travis Brown
la source
14
Chose intéressante, si vous écrivez le même code dans REPL au lieu de le générer dans une macro, cela fonctionne: scala> {classe finale anon {def x = 2}; new anon} res1: AnyRef {def x: Int} = anon $ 1 @ 5295c398. Merci pour le rapport! Je vais jeter un oeil cette semaine.
Eugene Burmako
1
Notez que j'ai déposé un problème ici .
Travis Brown
Non, pas un bloqueur, merci - l'astuce de classe anonyme supplémentaire a fonctionné pour moi chaque fois que j'en avais besoin. Je viens de remarquer quelques votes positifs sur la question et j'étais curieux de connaître le statut.
Travis Brown
3
pièce de membre de type est extrêmement facile -> wTF? vous êtes extrêmement craqué! dans le bon sens bien sûr :)
ZaoTaoBao
3
Il y a 153 votes positifs ici, et seulement 1 pour le numéro sur scala-lang.org . Plus de votes positifs pourraient le résoudre plus rapidement?
moodboom

Réponses:

9

Cette question est répondue en double par Travis ici . Il y a 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 types, notre héros décide de ce qui échappera à l'anonymat sombre et verra la lumière comme un membre du type structurel.

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

Si le rédacteur 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)
  }
}
som-snytt
la source
2
Je noterai simplement que je propose en fait la première solution de contournement dans cette question elle-même (c'est juste non quasiquoté ici). Je suis heureux que cette réponse résume la question - je pense que j'avais vaguement attendu que le bogue soit corrigé.
Travis Brown
@TravisBrown Je parie que vous avez aussi d'autres outils dans votre ceinture de chauve-souris. Merci pour le heads-up: j'ai supposé que votre AST était "le vieux truc d'accolades supplémentaires", mais je vois maintenant que les ClassDef / Apply ne sont pas enveloppés dans leur propre Block, comme cela se produit avec new $anon {}. Mon autre point anonà retenir est qu'à l'avenir, je ne les utiliserai plus dans les macros avec des quasiquotes ou des noms spéciaux similaires.
som-snytt
q La syntaxe "$ {s: String}" est un peu retardée, surtout si vous utilisez le paradis. Donc plus comme le mois prochain que la semaine prochaine.
Denys Shabalin
@ som-snytt @ denys-shabalin, y a-t-il une sorte de supercherie spéciale pour les types structurels a-la shapeless.Generic? Malgré mes meilleures intentions de forcer les Auxtypes de retour de modèle, le compilateur refuse de voir à travers le type structurel.
flavian