Qual é a maneira recomendada de passar os resultados de cálculos de macro para o tempo de execução?

9

Estou tentando construir uma abstração semelhante a SQL e estou com um problema.

Esta é uma "tabela de banco de dados" simplificada:

trait Coffee {
  def id: Long
  def name: String
  def brand: String
}

Esta é a minha abstração de consulta:

import language.experimental.macros

object Query {  
  def from[T] = 
    macro QueryMacros.fromMacro[T]
}

class From[T] {  
  def select[S](s: T => S): Select[T] =
    macro QueryMacros.selectMacro[T, S]
}

class Select[T] {
  def where(pred: T => Boolean): Where =
    macro QueryMacros.whereMacro[T]
}

class Where(val result: String)

Esta é minha implementação de macro:

import scala.reflect.macros.Context

object QueryMacros {
  val result = new StringBuilder

  def fromMacro[T : c.WeakTypeTag](c: Context): c.Expr[From[T]] = { 
    result ++= ("FROM " + c.weakTypeOf[T])
    c.universe.reify(new From[T])
  }

  def selectMacro[T : c.WeakTypeTag, S : c.WeakTypeTag](c: Context)(s: c.Expr[T => S]): c.Expr[Select[T]] = {
    result ++= ("SELECT " + s.tree)
    c.universe.reify(new Select[T])
  }

  def whereMacro[S](c: Context)(pred: c.Expr[S]): c.Expr[Where] = {
    result ++= ("WHERE " + pred.tree)
    c.universe.reify(new Where(result.toString))
  }
}

E este é o meu código de exemplo:

object Main extends App {
  println("Query start")
  val query = 
    Query.from[Coffee]
         .select(_.id)
         .where(_.brand == "FairTrade")

  println(query.result)
  println("Query end")
}

Compila e executa bem, mas a saída é:

Query start

Query end

Basicamente, result parece estar vazio. Eu esperava que ele segurasse as cordas acumuladas das árvores.

Como posso passar meus dados do estágio de compilação de macro para o próximo estágio, para que ele apareça em tempo de execução? Eu poderia, é claro, passar a string atual para o próximo método explicitamente, mas eu gostaria de evitar isso.

    
por soc 02.08.2012 в 14:19
fonte

3 respostas

3

Basicamente, você precisa ter uma abstração Queryable que: 1) forneça a API de coleta ( from , select , etc), 2) lembre os métodos que foram chamados, reificando as chamadas e acumulando-as dentro.

Este conceito é um pouco explicado em nossos slides do ScalaDays [1] e é implementado no Slick (que é open source) [2]. A propósito, no LINQ eles fazem o mesmo com métodos em Queryable reificando as chamadas e alimentando-as ao seu objeto que implementa IQueryable , por exemplo, como descrito em [3].

Links:

  1. link
  2. link
  3. link
por Eugene Burmako 02.08.2012 / 17:06
fonte
2

O problema não é passar as informações de uma chamada de macro para a próxima. Tudo isso acontece em tempo de compilação, então isso deve funcionar. O problema é com a macro que é chamada por último. Como ele retorna c.universe.reify(new Where(result.toString)) , new Where(result.toString) é chamado em tempo de execução. E então result estará vazio. O que você pode fazer é retornar c.Expr(tree) , onde tree aplica o construtor Where a um String literal que contém result.toString .

Além disso, você deve observar que seu código depende da ordem em que as chamadas de macro são compiladas. Se você tiver várias chamadas para essas macros em vários arquivos de código, result poderá conter informações de chamadas anteriores. Provavelmente seria melhor repensar toda a sua abordagem.

    
por Kim Stebel 02.08.2012 / 15:01
fonte
0

Como @Kim aponta a agregação das informações não é o problema, mas a expansão da macro gerará código que avalia result.toString no tempo de execução quando estiver realmente vazio. Eu tive um problema parecido com você e acabei fazendo o equivalente substituindo result.toString por resultExpr(c).splice

private def resultExpr(c :Context) = { 
  import c.universe._
   c.Expr[String](Literal(Constant(result.toString)))
}

(Como @Kim também aponta isso irá alimentar os resultados acumulados de todas as chamadas de macro de volta para o tempo de execução, então esteja atento!)

    
por subsub 23.08.2012 / 12:21
fonte