scala.meta

  • 23
    いいね
  • 0
    コメント

scala.meta


なぜ話題になっているのか

  • これまでScalaでマクロを書くにはscala-reflectを使っていた
  • ところがscala-reflectを使ったマクロはdeprecatedになるらしい
  • これからはscala.meta

scala.metaで何ができるか

  • Scalaのプログラムをいじることができる
def addStat(
  classDefn: Defn.Class, stat: Stat): Defn.Class = {
  val templ = classDefn.templ
  val stats = templ.stats.getOrElse(Seq.empty[Stat])
  val newStats = stats :+ stat
  val newTempl = templ.copy(stats = Some(newStats))
  classDefn.copy(templ = newTempl)
}

quasiquotes

scala> val tree = "x + y /* adds x and y */"
  .parse[Term]
  .get
tree: scala.meta.Term = x + y /* adds x and y */

scala> tree.syntax
res0: String = x + y /* adds x and y */

scala> tree.structure
res1: String = Term
  .ApplyInfix(
    Term.Name("x"),
    Term.Name("+"),
    Nil, 
    Seq(Term.Name("y")))

scala.metaでマクロを書く

  • 現時点ではmacro annotationがサポートされている
  • paradiseプラグインが必要

Hello world

import scala.meta._
class helloWorld extends StaticAnnotation {
  inline def apply(defn: Any): Any = meta {
    // compile時にhello worldされる
    println("hello world from macro!!")
    defn
  }
}

ToString

@ToString
case class User(
    id: Int,
    name: String,
    country: String,
    createdAt: LocalDateTime
)

ToString

inline def apply(defn: Any): Any = meta {
  ...
  defn match {
    case Term.Block(Seq(classDefn: Defn.Class, objDefn: Defn.Object)) =>
      Term.Block(Seq(addToStringMethod(classDefn), objDefn))
    case classDefn: Defn.Class =>
      val objName = Term.Name(classDefn.name.toString)
      val objDefn = q"object $objName"
      Term.Block(Seq(addToStringMethod(classDefn), objDefn))
    case _ => sys.error("error")
  }
}

ToString

def addToStringMethod(classDefn: Defn.Class): Stat = {
  val className = classDefn.name.toString
  val paramNames = classDefn.ctor.paramss
    .flatten.map { param =>
      val fieldName = param.name.toString
      q"""${Lit(fieldName)} + "=" + ${Term.Name(fieldName)}"""
    }
  val body = paramNames
    .reduce { (a, b) => q"""$a + ", " + $b""" }
  val method = 
    q"""override def toString(): String = 
          ${Lit(className)} + "(" + $body + ")""""
  addStat(classDefn, method)
}

実用例

  • scalafmt

実用例

@compile("example/src/main/resources/application.conf")
object path

object Example {

  def main(args: Array[String]): Unit = {
    val config = ConfigFactory.load()
    val serializer1 = config.getString(
      path.akka.actor.serializers.`akka-containers`.full)
    val serializer2 = config.getString(
      "akka.actor.serializers.akka-containers")
    assert(serializer1 == serializer2)
  }

}

parameter

  • annotationにはパラメータを渡せる
  • literalのみ
val configFile = this match {
  case q"new $_(${Lit(path)})" => new File(path.toString)
  case _ => abort("config file is not specified")
}

val config = ConfigFactory
  .parseFile(configFile)
  .resolve()

コード生成

val configTree = Seq(
  q"""
     abstract class ConfigTree(val name: String,
                               val full: String)
    """
)

def defConfigTree(name: String, path: String, fullPath: String): Defn.Object = {
  q"""object ${Term.Name(s"`$name`")} 
      extends ConfigTree(${Lit(path)}, ${Lit(fullPath)})"""
}

コード生成

  • 設定パスのツリーをたどっていく
def constructConfigTree[T: HasTemplate](
  obj: T,
  config: ConfigValue, 
  path: List[String]): T = {
  config.valueType() match {
    case ConfigValueType.OBJECT =>
      val configObject = 
        config.asInstanceOf[ConfigObject]
      val children = configObject.asScala.toMap
        .map { case (key, value) =>
          val child = defConfigTree(key, key, (path :+ key).mkString("."))
          constructConfigTree(child, value, path :+ key)
        }.to[Seq]
      addStats(obj, Seq.empty[Stat], children)
    case ConfigValueType.LIST |
         ConfigValueType.NUMBER |
         ConfigValueType.BOOLEAN |
         ConfigValueType.STRING |
         ConfigValueType.NULL =>
      obj
  }
}

生成されたコード

object path {
  abstract class ConfigTree(val name: String, val full: String)
  object `akka` extends ConfigTree("akka", "akka") {
    object `actor` extends ConfigTree("actor", "akka.actor") {
      object `serializers` extends ConfigTree("serializers", "akka.actor.serializers") {
        object `akka-containers` extends ConfigTree("akka-containers", "akka.actor.serializers.akka-containers")
      }
    }
  }
}
  • application.confがでかいといっぱい生成されて気分が良い

Documents


所感

  • macro annotationだけだと物足りない
    • case classからSQLの生成などをやってみたがjoinなどまでやろうとすると表現力不足
    • annotationをつけたdefinitionの中しかいじれない
      • Lombokの @CleanUp が無理そう
  • def macro待ち
  • IntelliJの対応が不十分
    • macroで生成されたメソッドなどを参照すると赤くなる