2013-01-14 231 views
12

我想編寫一個Scala宏,它將一個case類的實例作爲參數。所有可以傳遞給宏的對象都必須實現特定的標記特徵。傳遞給Scala宏的內省參數

如下片段展示標記性狀和兩個示例的情況下的類實施它:

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

現在,我想使用宏,以避免運行時反射和其螺紋不安全的沉重寫以下代碼:

object Test extends App { 

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

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

logDomain在不同的項目實施,類似如下:

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

宏的目的應該是生成的代碼 - 在運行時 - 輸出給定對象的所有值(idname),並將它們打印如下圖所示:

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

要做到這一點,我會動態檢查傳入的類型參數並確定其成員(vals)。然後我將不得不生成代表創建日誌輸出的代碼的AST。無論實現標記特徵「Domain」的特定對象傳遞給宏,宏應該工作。

此時我迷路了。如果有人能給我一個出發點或者指點我一些文檔,我將不勝感激。我相對較新的Scala,並沒有在Scala API文檔或宏指南中找到解決方案。

回答

14

清單的情況下類的訪問器是這樣一個共同的操作,當你與我傾向於保持一個方法,像這樣圍繞宏工作:

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 
} 

這將使我們所有的情況下訪問類方法符號爲A,如果有的話。請注意,我在這裏使用了一般的反射API--沒有必要製作這個宏特定的。

我們可以換這個方法了其他一些方便的東西:

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

現在我們可以非常簡明地編寫實際的宏代碼:

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

請注意,我們還需要跟蹤我們正在處理的具體案例類型的類型,但是類型推斷將在調用站點處處理 - 我們不需要明確指定類型參數。

現在,我們可以在您的案件類定義打開一個REPL,糊狀,然後寫:

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

或者:

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

如期望的那樣。

+0

你打我5分鐘! :) –

+0

非常感謝您的詳細解答!你的例子完全符合我的要求。今晚我會嘗試一下。你的解決方案的好處是使用類型參數和WeakTypeTag,它使代碼完全通用。它應該適用於任何實施「域」的案例類。 – MontChanais

7

從我所看到的,你需要解決兩個問題:1)從宏觀參數中獲取必要的信息,2)生成代表你需要的代碼的樹。

在Scala 2.10中,這些事情都是使用反射API完成的。請按照Is there a tutorial on Scala 2.10's reflection API yet?查看可用的文檔。

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 - 並感謝提醒有關「重複」。 –

+0

非常感謝這個詳細的答案。我越來越喜歡Scala,新的反射和宏支持非常棒。我將更多地使用它,並嘗試生成一些代碼,使用提取的值vals實例化不同的對象。 – MontChanais