Макросы Scala: создание карты из полей класса в Scala
Допустим, у меня много похожих классов данных. Вот пример класса User
который определяется следующим образом:
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)
Что я делаю неправильно? Я думаю, что компилятор пытается также оценить идентификаторы полей при создании выражения, но я не знаю, как правильно их вернуть в выражении. Не могли бы вы показать мне, как это сделать?
Большое спасибо заранее.
3 ответа
Обратите внимание, что это можно сделать гораздо более элегантно без 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
,
На карте есть отличная запись блога в / из класса преобразования с использованием макросов.
Начало Scala 2.13
, case class
ES (которые являются реализацией Product
) теперь снабжены методом productElementNames, который возвращает итератор над именами их полей.
Путем архивирования имен полей со значениями полей, полученными с помощью productIterator, можно получить Map
вне зависимости от класса дела:
// val user = User("Foo", 25, List("Lorem", "Ipsum"))
(user.productElementNames zip user.productIterator).toMap
// Map[String, Any] = Map("name" -> "Foo", "age" -> 25, "posts" -> List("Lorem", "Ipsum"))