2013-01-14 19 views
12

Chciałbym zaprogramować makro Scala, które pobiera wystąpienie klasy case jako argumentu. Wszystkie obiekty, które można przekazać do makra, muszą zaimplementować określoną cechę znacznika.Introspect argument przekazany do makro Scala

Poniższy fragment pokazuje cecha marker i dwie klasy przykładowym przypadku wdrożenia go:

trait Domain 
case class Country(id: String, name: String) extends Domain 
case class Town(id: String, longitude: Double, latitude: Double) extends Domain 

Teraz chciałbym napisać następujący kod za pomocą makra, aby uniknąć ciężkość refleksji wykonawczego i jego wątku unsafety:

object Test extends App { 

    // instantiate example domain object 
    val myCountry = Country("CH", "Switzerland") 

    // this is a macro call 
    logDomain(myCountry) 
} 

makro logDomain realizowany jest w innym projekcie i wygląda podobnie do:

object Macros { 
    def logDomain(domain: Domain): Unit = macro logDomainMacroImpl 

    def logDomainMacroImpl(c: Context)(domain: c.Expr[Domain]): c.Expr[Unit] = { 
    // Here I would like to introspect the argument object but do not know how? 
    // I would like to generate code that prints out all val's with their values 
    } 
} 

Celem makro powinno być generowania kodu, który - w czasie pracy - wyjścia wszystkich wartości (id i name) danego obiektu i wyświetla je jak przedstawiono poniżej:

id (String) : CH 
name (String) : Switzerland 

Aby to osiągnąć, muszę do dynamicznego sprawdzania argumentu przekazanego typu i określania jego członków (vals). Następnie musiałbym wygenerować AST reprezentujący kod, który tworzy dane wyjściowe dziennika. Makro powinno działać niezależnie od tego, jaki konkretny obiekt implementuje cechę znacznika "Domena" jest przekazywany do makra.

W tym momencie jestem zagubiony. Byłbym wdzięczny, gdyby ktoś dał mi punkt wyjścia lub wskazał mi jakąś dokumentację? Jestem stosunkowo nowy w Scali i nie znalazłem rozwiązania w dokumentach API Scala lub w przewodniku Makro.

Odpowiedz

14

Wymieniając akcesorów o klasie sprawa jest takiego wspólnego działania podczas pracy z makrami, że staram się zachować metodę tak dookoła:

def accessors[A: u.WeakTypeTag](u: scala.reflect.api.Universe) = { 
    import u._ 

    u.weakTypeOf[A].declarations.collect { 
    case acc: MethodSymbol if acc.isCaseAccessor => acc 
    }.toList 
} 

To da nam wszystkie akcesor klasy przypadek symbole metod dla A, jeśli ma jakieś. Zwróć uwagę, że używam tutaj ogólnego interfejsu API do refleksji - nie ma potrzeby, aby ten makrokomputer był jeszcze określony.

możemy owinąć tę metodę z jakiegoś innego materiału Wygoda:

trait ReflectionUtils { 
    import scala.reflect.api.Universe 

    def accessors[A: u.WeakTypeTag](u: Universe) = { 
    import u._ 

    u.weakTypeOf[A].declarations.collect { 
     case acc: MethodSymbol if acc.isCaseAccessor => acc 
    }.toList 
    } 

    def printfTree(u: Universe)(format: String, trees: u.Tree*) = { 
    import u._ 

    Apply(
     Select(reify(Predef).tree, "printf"), 
     Literal(Constant(format)) :: trees.toList 
    ) 
    } 
} 

A teraz możemy napisać rzeczywisty kod makro dość zwięźle:

trait Domain 

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

    def log[D <: Domain](domain: D): Unit = macro log_impl[D] 
    def log_impl[D <: Domain: c.WeakTypeTag](c: Context)(domain: c.Expr[D]) = { 
    import c.universe._ 

    if (!weakTypeOf[D].typeSymbol.asClass.isCaseClass) c.abort(
     c.enclosingPosition, 
     "Need something typed as a case class!" 
    ) else c.Expr(
     Block(
     accessors[D](c.universe).map(acc => 
      printfTree(c.universe)(
      "%s (%s) : %%s\n".format(
       acc.name.decoded, 
       acc.typeSignature.typeSymbol.name.decoded 
      ), 
      Select(domain.tree.duplicate, acc.name) 
     ) 
     ), 
     c.literalUnit.tree 
    ) 
    ) 
    } 
} 

Zauważ, że wciąż musimy śledzić konkretnego typu klasy sprawy, z którą mamy do czynienia, ale w miejscu wywołania zajmiemy się wnioskiem typu - nie będziemy musieli jawnie określać parametru typu.

Teraz możemy otworzyć rEPL, pasty w swoich definicjach klas przypadek, a następnie napisać następujące:

scala> Macros.log(Town("Washington, D.C.", 38.89, 77.03)) 
id (String) : Washington, D.C. 
longitude (Double) : 38.89 
latitude (Double) : 77.03 

czyli

scala> Macros.log(Country("CH", "Switzerland")) 
id (String) : CH 
name (String) : Switzerland 

zgodnie z zapotrzebowaniem.

+0

Pokonałeś mnie 5 minut! :) –

+0

Wielkie dzięki za szczegółową odpowiedź! Twój przykład robi dokładnie to, czego szukałem. Wypróbuję to dziś wieczorem. To, co jest dobre w twoim rozwiązaniu, to użycie parametru type i WeakTypeTag, który sprawia, że ​​kod jest całkowicie ogólny. Powinien działać dla każdej klasy przypadków implementującej "domenę". – MontChanais

7

Z tego co widzę, należy rozwiązać dwa problemy: 1) uzyskać niezbędne informacje z argumentu makra, 2) wygenerować drzewa, które reprezentują potrzebny kod.

W programie Scala 2.10 czynności te wykonuje się za pomocą interfejsu API do odczytywania. Postępuj zgodnie z Is there a tutorial on Scala 2.10's reflection API yet?, aby zobaczyć, jaka dokumentacja jest dostępna dla niego.

import scala.reflect.macros.Context 
import language.experimental.macros 

trait Domain 
case class Country(id: String, name: String) extends Domain 
case class Town(id: String, longitude: Double, latitude: Double) extends Domain 

object Macros { 
    def logDomain(domain: Domain): Unit = macro logDomainMacroImpl 

    def logDomainMacroImpl(c: Context)(domain: c.Expr[Domain]): c.Expr[Unit] = { 
    import c.universe._ 

    // problem 1: getting the list of all declared vals and their types 
    // * declarations return declared, but not inherited members 
    // * collect filters out non-methods 
    // * isCaseAccessor only leaves accessors of case class vals 
    // * typeSignature is how you get types of members 
    //  (for generic members you might need to use typeSignatureIn) 
    val vals = typeOf[Country].declarations.toList.collect{ case sym if sym.isMethod => sym.asMethod }.filter(_.isCaseAccessor) 
    val types = vals map (_.typeSignature) 

    // problem 2: generating the code which would print: 
    // id (String) : CH 
    // name (String) : Switzerland 
    // 
    // usually reify is of limited usefulness 
    // (see https://stackoverflow.com/questions/13795490/how-to-use-type-calculated-in-scala-macro-in-a-reify-clause) 
    // but here it's perfectly suitable 
    // a subtle detail: `domain` will be possibly used multiple times 
    // therefore we need to duplicate it 
    val stmts = vals.map(v => c.universe.reify(println(
     c.literal(v.name.toString).splice + 
     "(" + c.literal(v.returnType.toString).splice + ")" + 
     " : " + c.Expr[Any](Select(domain.tree.duplicate, v)).splice)).tree) 

    c.Expr[Unit](Block(stmts, Literal(Constant(())))) 
    } 
} 
+2

+ 1-i dziękuję za przypomnienie o 'duplikacie'. –

+0

Bardzo dziękuję za tę szczegółową odpowiedź. Bardziej lubię Scalę, a nowe wsparcie dla Reflection i Macro jest świetne. Pobiorę się z nim i spróbuję wygenerować kod, który tworzy inny obiekt przy użyciu wyodrębnionych wartości wielkości liter. – MontChanais

Powiązane problemy