Obtener un tipo estructural con los métodos de una clase anónima de una macro

181

Supongamos que deseamos escribir una macro que defina una clase anónima con algunos miembros de tipo o métodos, y luego cree una instancia de esa clase que esté tipada estáticamente como un tipo estructural con esos métodos, etc. Esto es posible con el sistema de macros en 2.10. 0, y la parte del miembro tipo es extremadamente fácil:

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)
    ))
  }
}

(¿Dónde ReflectionUtilshay un rasgo de conveniencia que proporciona mi constructormétodo?)

Esta macro nos permite especificar el nombre del miembro de tipo de la clase anónima como un literal de cadena:

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

Tenga en cuenta que está escrito correctamente. Podemos confirmar que todo funciona como se esperaba:

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

Ahora supongamos que intentamos hacer lo mismo con un método:

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)
  ))
}

Pero cuando lo probamos, no obtenemos un tipo estructural:

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

Pero si colocamos una clase anónima adicional allí:

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)
  ))
}

Funciona:

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

scala> res0.test
res1: Int = 42

Esto es extremadamente útil, te permite hacer cosas como esta , por ejemplo, pero no entiendo por qué funciona, y la versión del miembro tipo funciona, pero no bar. Sé que esto puede no ser un comportamiento definido , pero ¿tiene algún sentido? ¿Hay una forma más limpia de obtener un tipo estructural (con los métodos en él) de una macro?

Travis Brown
fuente
14
Curiosamente, si escribe el mismo código en REPL en lugar de generarlo en una macro, funciona: scala> {clase final anon {def x = 2}; nuevo anon} res1: AnyRef {def x: Int} = anon $ 1 @ 5295c398. Gracias por el informe! Echaré un vistazo esta semana.
Eugene Burmako
1
Tenga en cuenta que he presentado un problema aquí .
Travis Brown
No, no es un bloqueador, gracias, el truco de clase anónimo adicional me ha funcionado siempre que lo necesitaba. Acabo de notar un par de votos a favor sobre la pregunta y tenía curiosidad sobre el estado.
Travis Brown
3
La parte miembro tipo es extremadamente fácil -> wTF? eres extremadamente crack! en el buen sentido, por supuesto :)
ZaoTaoBao
3
Aquí hay 153 votos a favor, y solo 1 para el problema en scala-lang.org . ¿Más votos a favor allí podrían resolverlo más rápido?
moodboom

Respuestas:

9

Esta pregunta es respondida por duplicado por Travis aquí . Hay enlaces al tema en el rastreador y a la discusión de Eugene (en los comentarios y la lista de correo).

En la famosa sección "Skylla and Charybdis" del verificador de tipos, nuestro héroe decide qué escapará al oscuro anonimato y verá la luz como un miembro del tipo estructural.

Hay un par de formas de engañar al verificador de tipos (que no implican la estratagema de Odiseo de abrazar a una oveja). Lo más simple es insertar una declaración ficticia para que el bloque no se vea como una clase anónima seguida de su instanciación.

Si el usuario nota que usted es un término público al que no hace referencia el exterior, lo hará privado.

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
fuente
2
Solo tendré en cuenta que en realidad proporciono la primera solución en esta pregunta en sí (aquí no está quasiquotada). Me alegra que esta respuesta finalice la pregunta: creo que había estado esperando vagamente a que se solucionara el error.
Travis Brown
@TravisBrown Apuesto a que también tienes otras herramientas en tu Bat Belt. Gracias por el aviso: asumí que su AST era "el viejo truco de llaves extra", pero ahora veo que ClassDef / Apply no están envueltos en su propio bloque, como sucede con new $anon {}. Mi otra conclusión es que en el futuro no lo anonusaré en macros con cuasiquotes o nombres especiales similares.
som-snytt
q La sintaxis "$ {s: String}" se retrasa un poco, especialmente si está usando el paraíso. Así que más bien el próximo mes en lugar de la próxima semana.
Denys Shabalin
@ som-snytt @ denys-shabalin, ¿hay algún tipo especial de truco para los tipos estructurales a-la shapeless.Generic? A pesar de mis mejores intenciones de forzar Auxtipos de retorno de patrones, el compilador se niega a ver a través del tipo estructural.
Flavian