Skip to content

Instantly share code, notes, and snippets.

@fpopic
Last active June 1, 2019 21:51
Show Gist options
  • Save fpopic/0ea9f59310fdbe988b1dddd1b0105f1b to your computer and use it in GitHub Desktop.
Save fpopic/0ea9f59310fdbe988b1dddd1b0105f1b to your computer and use it in GitHub Desktop.
Implicit Macro TypeClass Problem for any case class (nested as well).
// Would like to serialize any Scala case class (could be nested as well) to Map[String, Any]
case class N(j: Int)
case class A(i: Int, s: Some[Int], l: List[Int], n: N)
// Want to generate:
object N extends Mappable[N] {
def toMap(n: N): Map[String, Any] = {
Map(
"j" -> n.j
)
}
}
object A extends Mappable[A] {
def toMap(a: A): Map[String, Any] = {
Map(
"i" -> a.i,
"s" -> a.s,
"l" -> a.l,
"n" -> N.toMap(n)
)
}
}
/*
For later:
- before adding pair to map check if null
- convert Scala to Java collections
- support Maps as fields
- extract Some/Option
- convert Timestamp-like fields to String Literals
- add field customizations using annotations or some context (like customizationsMap)
*/
package com.github.fpopic.scalamacros
import scala.language.experimental.macros
import scala.reflect.macros.blackbox
// 0. Define Type Class
trait Mappable[T] {
def toMap(t: T): Map[String, Any]
}
object Mappable extends MappableLowPriorityImplicits {
// 2. Implicit method that triggers the macro
// HighPriorityImplicits
// LowPriorityMacros
implicit def caseClassMappable[T]: Mappable[T] = macro materializeMappableImpl[T]
// 1. Caller initiates type class implicits resolution
def mapify[T](t: T)(implicit m: Mappable[T]): Map[String, Any] = m.toMap(t)
}
trait MappableLowPriorityImplicits {
// 3. Macro that generates for any case class Mappable implementation
def materializeMappableImpl[T: c.WeakTypeTag](c: blackbox.Context): c.Expr[Mappable[T]] = {
import c.universe._
val tpe: c.universe.Type = weakTypeOf[T]
println(s"Mappable: $tpe")
def getPrimaryConstructorMembers(_tpe: c.Type): Seq[c.Symbol] =
_tpe.decls.collectFirst {
case m: MethodSymbol if m.isPrimaryConstructor => m
}.get.paramLists.head
val mapEntries: Seq[c.Tree] =
getPrimaryConstructorMembers(tpe).map { field =>
val fName = field.name.decodedName.toString
val fTerm = field.name.toTermName
// doesn't work with tType.decl(field.name).typeSignature
val fType = field.typeSignature
fType match {
case t if t =:= weakTypeOf[Int] =>
println(s"$fName : $fType")
q"$fName -> (t.$fTerm + 100)"
case t if t =:= weakTypeOf[Some[Int]] =>
println(s"$fName : $fType")
q"$fName -> t.$fTerm"
case t if t =:= weakTypeOf[List[Int]] =>
println(s"$fName : $fType")
q"$fName -> t.$fTerm"
case n if n.baseClasses.contains(weakTypeOf[Product].typeSymbol) =>
println(s"$fName : $fType")
q"$fName -> implicitly[Mappable[$n]].toMap(t.$fTerm)"
case t =>
c.abort(c.enclosingPosition, s"Type $t not supported.")
}
}
val ret =
q"""new Mappable[$tpe] {
def toMap(t: $tpe): Map[String, Any] = Map(..$mapEntries)
}
"""
println(s"Ret: $ret")
c.Expr[Mappable[T]](ret)
}
}
package com.github.fpopic.scalamacros
case class N(j: Int, k: Int)
case class A(i: Int, s: Some[Int], l: List[Int], n: N)
object DefMacroUsage {
def main(args: Array[String]): Unit = {
import Mappable._
val a: Map[String, Any] = mapify(
A(
i = 1,
s = Some(2),
l = List(3, 4),
n = N(
j = 5,
k = 7
)
)
)
println(a)
}
}
[info] Compiling 4 Scala sources to /home/fpopic/Projects/IdeaProjects/github/fpopic/scala-code-generation-poc/scala-macros-usage/target/scala-2.13.0-M3/classes ...
Mappable: com.github.fpopic.scalamacros.A
i : Int
s : Some[Int]
l : List[Int]
n : com.github.fpopic.scalamacros.N
Ret: {
final class $anon extends Mappable[com.github.fpopic.scalamacros.A] {
def <init>() = {
super.<init>();
()
};
def toMap(t: com.github.fpopic.scalamacros.A): Map[String, Any] = Map("i".$minus$greater(t.i.$plus(100)), "s".$minus$greater(t.s), "l".$minus$greater(t.l), "n".$minus$greater(implicitly[Mappable[com.github.fpopic.scalamacros.N]].toMap(t.n)))
};
new $anon()
}
Mappable: com.github.fpopic.scalamacros.N
j : Int
k : Int
Ret: {
final class $anon extends Mappable[com.github.fpopic.scalamacros.N] {
def <init>() = {
super.<init>();
()
};
def toMap(t: com.github.fpopic.scalamacros.N): Map[String, Any] = Map("j".$minus$greater(t.j.$plus(100)), "k".$minus$greater(t.k.$plus(100)))
};
new $anon()
}
[info] Done compiling.
[success] Total time: 18 s, completed Jun 1, 2019 11:39:14 PM

[info] Running com.github.fpopic.scalamacros.DefMacroUsage

Map(i -> 101, s -> Some(2), l -> List(3, 4), n -> Map(j -> 105, k -> 107))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment