Obtendo um tipo estrutural com métodos de uma classe anônima a partir de uma macro

181

Suponha que desejemos escrever uma macro que defina uma classe anônima com alguns membros ou métodos de tipo e, em seguida, crie uma instância dessa classe que seja estaticamente digitada como um tipo estrutural com esses métodos, etc. Isso é possível com o sistema de macro na versão 2.10. 0 e a parte do membro de tipo é extremamente 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)
    ))
  }
}

(Onde ReflectionUtilsestá um traço de conveniência que fornece meu constructormétodo.)

Essa macro nos permite especificar o nome do membro do tipo da classe anônima como uma literal de cadeia de caracteres:

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

Observe que foi digitado adequadamente. Podemos confirmar que tudo está funcionando como esperado:

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

Agora, suponha que tentemos fazer a mesma coisa com um 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)
  ))
}

Mas quando tentamos, não temos um tipo estrutural:

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

Mas se colocarmos uma classe anônima extra lá:

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

Isso é extremamente útil - permite fazer coisas como essa , por exemplo - mas não entendo por que funciona e a versão do membro de tipo funciona, mas não bar. Eu sei que isso pode não ser um comportamento definido , mas isso faz algum sentido? Existe uma maneira mais limpa de obter um tipo estrutural (com os métodos nele) a partir de uma macro?

Travis Brown
fonte
14
Curiosamente, se você escrever o mesmo código no REPL em vez de gerá-lo em uma macro, ele funcionará: scala> {classe final anon {def x = 2}; novo ano} res1: AnyRef {def x: Int} = ano $ 1 @ 5295c398. Obrigado pelo relatório! Vou dar uma olhada esta semana.
Eugene Burmako
1
Observe que eu arquivei um problema aqui .
Travis Brown
Não, não um bloqueador, obrigado - o truque extra anônimo da classe funcionou para mim sempre que eu precisava. Acabei de notar alguns votos positivos sobre a questão e fiquei curioso sobre o status.
Travis Brown
3
tipo de parte do membro é extremamente fácil -> wTF? você está extremamente rachar no bom caminho, é claro :)!
ZaoTaoBao
3
Existem 153 votações anteriores aqui e apenas 1 para a edição em scala-lang.org . Mais votos positivos podem resolvê-lo mais rapidamente?
Moodboom 14/11/2013

Respostas:

9

Esta pergunta é respondida em duplicado por Travis aqui . Existem links para o problema no rastreador e na discussão de Eugene (nos comentários e na lista de discussão).

Na famosa seção "Skylla e Charybdis" do verificador de tipos, nosso herói decide o que deve escapar do anonimato sombrio e vê a luz como um membro do tipo estrutural.

Existem algumas maneiras de enganar o verificador de tipos (que não implica a estratégia de Odisseu de abraçar uma ovelha). O mais simples é inserir uma instrução fictícia para que o bloco não pareça uma classe anônima seguida por sua instanciação.

Se o digitador perceber que você é um termo público que não é referenciado por fora, ele o tornará 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
fonte
2
Vou apenas observar que, na verdade, forneço a primeira solução alternativa nesta questão (é apenas sem quasiquotes aqui). Fico feliz em ter esta resposta para concluir a pergunta - acho que estava vagamente esperando que o bug fosse corrigido.
Travis Brown
@TravisBrown Aposto que você também tem outras ferramentas em seu Bat Belt. Thx para o heads-up: Eu assumi que o seu AST era "o velho truque de chaves extras", mas agora vejo que o ClassDef / Apply não está envolvido em seu próprio bloco, como acontece com new $anon {}. Minha outra anonsugestão é que, no futuro, não usarei macros com quasiquotes ou nomes especiais semelhantes.
som-snytt
q A sintaxe "$ {s: String}" fica um pouco atrasada, especialmente se você estiver usando o paraíso. Então, mais como no próximo mês, e não na próxima semana.
Denys Shabalin
@ som-snytt @ denys-shabalin, existe um tipo especial de truque para os tipos estruturais a-la shapeless.Generic? Apesar das minhas melhores intenções de forçar os Auxtipos de retorno de padrão, o compilador se recusa a ver através do tipo estrutural.
precisa