Scala Macros: Making a Map out of fields of a class in Scala

后端 未结 3 684
孤独总比滥情好
孤独总比滥情好 2020-11-29 19:42

Let\'s say that I have a lot of similar data classes. Here\'s an example class User which is defined as follows:

case class User (name: String,          


        
相关标签:
3条回答
  • 2020-11-29 19:52

    Note that this can be done much more elegantly without the toString / c.parse business:

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

    Note also that you need the c.resetAllAttrs bit if you want to be able to write the following:

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

    Without it you'll get a confusing ClassCastException in this situation.

    By the way, here's a trick that I've used to avoid the extra type parameter in e.g. user.toMap[User] when writing macros like this:

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

    Now we can write the following:

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

    And don't need to specify that we're talking about a User.

    0 讨论(0)
  • 2020-11-29 19:58

    There is an excellent blog post on map to/from case class conversion using macros.

    0 讨论(0)
  • 2020-11-29 20:10

    Starting Scala 2.13, case classes (which are an implementation of Product) are now provided with a productElementNames method which returns an iterator over their field's names.

    By zipping field names with field values obtained with productIterator one can obtained a Map out of whatever case class:

    // 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"))
    
    0 讨论(0)
提交回复
热议问题