2013-06-20 83 views
29

假設我有很多類似的數據類。下面是其被定義爲如下的示例類UserScala宏:在Scala中創建一個類的字段

case class User (name: String, age: Int, posts: List[String]) { 
    val numPosts: Int = posts.length 

    ... 

    def foo = "bar" 

    ... 
} 

我對自動創建(在編譯時)的方法,其返回的方式一個Map每個字段名稱被映射到它的值,當它在運行時被調用。對於上面的例子,讓我們說,我的方法被稱爲toMap

val myUser = User("Foo", 25, List("Lorem", "Ipsum")) 

myUser.toMap 

應該返回

Map("name" -> "Foo", "age" -> 25, "posts" -> List("Lorem", "Ipsum"), "numPosts" -> 2) 

你將如何與宏做到這一點?

這裏是我做了什麼:首先,我創建了一個Model類作爲我所有的數據類的父類,並在那裏實現的方法是這樣的:

abstract class Model { 
    def toMap[T]: Map[String, Any] = macro toMap_impl[T] 
} 

class User(...) extends Model { 
    ... 
} 

然後我在一個定義的宏實現獨立Macros對象:

object Macros { 
    import scala.language.experimental.macros 
    import scala.reflect.macros.Context 
    def getMap_impl[T: c.WeakTypeTag](c: Context): c.Expr[Map[String, Any]] = { 
    import c.universe._ 

    val tpe = weakTypeOf[T] 

    // Filter members that start with "value", which are val fields 
    val members = tpe.members.toList.filter(m => !m.isMethod && m.toString.startsWith("value")) 

    // Create ("fieldName", field) tuples to construct a map from field names to fields themselves 
    val tuples = 
     for { 
     m <- members 
     val fieldString = Literal(Constant(m.toString.replace("value ", ""))) 
     val field = Ident(m) 
     } yield (fieldString, field) 

    val mappings = tuples.toMap 

    /* Parse the string version of the map [i.e. Map("posts" -> (posts), "age" -> (age), "name" -> (name))] to get the AST 
    * for the map, which is generated as: 
    * 
    * Apply(Ident(newTermName("Map")), 
    * List(
    *  Apply(Select(Literal(Constant("posts")), newTermName("$minus$greater")), List(Ident(newTermName("posts")))), 
    *  Apply(Select(Literal(Constant("age")), newTermName("$minus$greater")), List(Ident(newTermName("age")))), 
    *  Apply(Select(Literal(Constant("name")), newTermName("$minus$greater")), List(Ident(newTermName("name")))) 
    * ) 
    *) 
    * 
    * which is equivalent to Map("posts".$minus$greater(posts), "age".$minus$greater(age), "name".$minus$greater(name)) 
    */ 
    c.Expr[Map[String, Any]](c.parse(mappings.toString)) 
    } 
} 

然而,當我嘗試編譯它,我得到SBT此錯誤:

[error] /Users/emre/workspace/DynamoReflection/core/src/main/scala/dynamo/Main.scala:9: not found: value posts 
[error]  foo.getMap[User] 
[error]    ^

Macros.scala正在編譯中。這裏是我的Build.scala的片段:

lazy val root: Project = Project(
    "root", 
    file("core"), 
    settings = buildSettings 
) aggregate(macros, core) 

    lazy val macros: Project = Project(
    "macros", 
    file("macros"), 
    settings = buildSettings ++ Seq(
     libraryDependencies <+= (scalaVersion)("org.scala-lang" % "scala-reflect" % _)) 
) 

    lazy val core: Project = Project(
    "core", 
    file("core"), 
    settings = buildSettings 
) dependsOn(macros) 

我在做什麼錯?我認爲編譯器在創建表達式時也會嘗試評估字段標識符,但我不知道如何在表達式中正確返回它們。你能告訴我該怎麼做嗎?

非常感謝。

+0

而不是使用宏,這可能會更容易http://stackoverflow.com/questions/1226555/case-class-to-map-in-scala – Noah

+0

@Noah,是的,看到一個已經。但我有興趣在編譯宏時儘管如此。謝謝您的幫助! – Emre

+2

而不只是'訂貨號(newTermName(職位))'你需要使用'選擇(c.prefix.tree,newTermName( 「上崗」))'。 –

回答

31

注意,這可以更優雅的完成,無需toString/c.parse業務:

import scala.language.experimental.macros 

abstract class Model { 
    def toMap[T]: Map[String, Any] = macro Macros.toMap_impl[T] 
} 

object Macros { 
    import scala.reflect.macros.Context 

    def toMap_impl[T: c.WeakTypeTag](c: Context) = { 
    import c.universe._ 

    val mapApply = Select(reify(Map).tree, newTermName("apply")) 

    val pairs = weakTypeOf[T].declarations.collect { 
     case m: MethodSymbol if m.isCaseAccessor => 
     val name = c.literal(m.name.decoded) 
     val value = c.Expr(Select(c.resetAllAttrs(c.prefix.tree), m.name)) 
     reify(name.splice -> value.splice).tree 
    } 

    c.Expr[Map[String, Any]](Apply(mapApply, pairs.toList)) 
    } 
} 

還要注意的是你需要的c.resetAllAttrs位,如果你希望能夠寫出如下:

User("a", 1, Nil).toMap[User] 

沒有它你會在這種情況下得到一個令人困惑的ClassCastException

順便說一下,這裏有一個技巧,我已經用來避免例如額外的類型參數。 user.toMap[User]書寫時宏是這樣的:

import scala.language.experimental.macros 

trait Model 

object Model { 
    implicit class Mappable[M <: Model](val model: M) extends AnyVal { 
    def asMap: Map[String, Any] = macro Macros.asMap_impl[M] 
    } 

    private object Macros { 
    import scala.reflect.macros.Context 

    def asMap_impl[T: c.WeakTypeTag](c: Context) = { 
     import c.universe._ 

     val mapApply = Select(reify(Map).tree, newTermName("apply")) 
     val model = Select(c.prefix.tree, newTermName("model")) 

     val pairs = weakTypeOf[T].declarations.collect { 
     case m: MethodSymbol if m.isCaseAccessor => 
      val name = c.literal(m.name.decoded) 
      val value = c.Expr(Select(model, m.name)) 
      reify(name.splice -> value.splice).tree 
     } 

     c.Expr[Map[String, Any]](Apply(mapApply, pairs.toList)) 
    } 
    } 
} 

現在我們可以寫:

scala> println(User("a", 1, Nil).asMap) 
Map(name -> a, age -> 1, posts -> List()) 

,並不需要指定,我們正在談論的是User

+0

爲什麼resetAllAttrs?看起來在這裏不應該有必要。 –

+0

它的工作原理沒有resetAllAttrs。 感謝您的好評。有一件事,你的實現只輸出在構造函數中定義的vals(即case訪問器)。我用isAccessor來代替。我似乎以前錯過了這種方法。 – Emre

+0

啊,對 - 我已經在第二個例子中刪除了'resetAllAttrs'(雖然在第一個例子中它肯定是必需的)。例如,我不確定非案例類成員,因爲'numPosts'沒有出現在你想要的輸出中。 –