scalametaに入門する
2017-02-19
QiitaScalametaprogrammingmetascalameta は inline macro と呼ばれる macro annotation のためのツールキット。 型安全に macro 出来る。
公式とチュートリアルは以下。
setup
まずは使えるようにするために build.sbt に追記。
libraryDependencies += "org.scalameta" %% "scalameta" % "1.5.0"
ちなみにAmmonite-REPLを使っているなら以下。
import $ivy.`org.scalameta::scalameta:1.5.0`
import
import はこう。 とりあえずワイルドカードで import してしまう。
import scala.meta._
以下のサンプルでは省略している。
scalameta の基礎概念
Tokens
Scala の文を切り分けたもの。
meta の基本になる。
とりあえずxxx.syntax
としておけば、その token を表現する文字列が手に入ることをおぼえておけば便利。
@ "val x = 1".tokenize
res7: Tokenized = val x = 1
@ "val x = 1".tokenize.get
res8: Tokens = Tokens(, val, , x, , =, , 1, )
@ "val x = 1".tokenize.get.syntax
res9: String = "val x = 1"
@ "val x = 1".tokenize.get.structure
res10: String = "Tokens(BOF [0..0), val [0..3), [3..4), x [4..5), [5..6), = [6..7), [7..8), 1 [8..9), EOF [9..9))"
@ val tokens = "val x = 1".tokenize.get
tokens: Tokens = Tokens(, val, , x, , =, , 1, )
@ tokens.map { x => f"${x.structure}%10s -> ${x.getClass}" }.mkString("\n")
res13: String = """
BOF [0..0) -> class scala.meta.tokens.Token$BOF
val [0..3) -> class scala.meta.tokens.Token$KwVal
[3..4) -> class scala.meta.tokens.Token$Space
x [4..5) -> class scala.meta.tokens.Token$Ident
[5..6) -> class scala.meta.tokens.Token$Space
= [6..7) -> class scala.meta.tokens.Token$Equals
[7..8) -> class scala.meta.tokens.Token$Space
1 [8..9) -> class scala.meta.tokens.Token$Constant$Int
EOF [9..9) -> class scala.meta.tokens.Token$EOF
"""
@ "val x = \"10".tokenize.get
<input>:1: error: unclosed string literal
val x = "10
^
"""
Quasiquotes
準クオート。scalameta というより macro 全般に共通する。 http://docs.scala-lang.org/overviews/quasiquotes/intro.html 文字列から構文木を生成してくれる
文法はq"xxx"
。
string-interpolation と同じように使用できる。
クオートした文字列がどういうものか、val
なのかdef
なのか{???}
なのか...、で
型が確定し、クオート内で展開するにも文法が正しくないとコンパイルエラーになる。
ちなみにその型の実装はこの辺で見れる scalameta/scalameta/Trees.scala。
注意点はscala.Seq
ではなくてscala.collection.immutable.Seq
を使うこと。
使い方のサンプルはこんな感じ。
@ val s = collection.immutable.Seq(q"val x = 10", q"def double(value: Int) = value * 2")
res38: Seq[Defn] = List(val x = 10, def double(value: Int) = value * 2)
@ q"""..$s"""
res39: Term.Block = {
val x = 10
def double(value: Int) = value * 2
}
@ q"""start { ..$s } end"""
res21: Term.Select = start {
val x = 10
def double(value: Int) = value * 2
}.end
@ val var10 = q"x = 10"
var10: Term.Assign = x = 10
@ val var20 = q"x = 20"
var20: Term.Assign = x = 20
@ val ifVar = q"if (true) $var10 else $var20"
ifVar: Term.If = if (true) x = 10 else x = 20
unapply で取り出すことも出来る。
@ val statement = q"val x = if (y > 0) 100 else 0"
statement: Defn.Val = val x = if (y > 0) 100 else 0
@ val q"val $x = if ($cond) $trueResult else $falseResult" = statement
x: Pat = x
cond: Term = y > 0
trueResult: Term = 100
falseResult: Term = 0
文字列をparse
して構文木も取れる。
@ val statement = "val x = if (y > 0) 100 else 0".parse[Stat].get
statement: Stat = val x = if (y > 0) 100 else 0
@ val traitSource = "trait Hoge { def double(n: Int) = n * 2 }".parse[Source].get
traitSource: Source = trait Hoge { def double(n: Int) = n * 2 }
@ val source"trait $traitName { ..$body }" = traitSource
traitName: Type.Name = Hoge
body: collection.immutable.Seq[Stat] = List(def double(n: Int) = n * 2)
ここでsource""
という準クオートが出てきているが、他にもたくさんあり、scalameta/Api.scalaに実装がある。
などなど色々あるが、token と準クオート(q""
)を抑えておけば、とりあえず手を動かせる。
macro annotation を実装してみる
scalameta で Macro Annotation を実装する。 A Whirlwind Tour of scala.meta
普通の class だけどtoString
は case class のようにしたい、というのはよくあるはず。
Java なら Lombok で[@ToString]がある。
これに近いことを scalameta でやってみる。
実装
scala.annotation.StaticAnnotation
を extends した class を定義する。
クラスの本体はinline def apply(defn: Any): Any = meta { ??? }
が大体テンプレ。
内部で使うSeq
はcollection.immutable.Seq
なので import しておくこと。
全体像は以下。
import scala.meta._
import scala.collection.immutable.Seq
class ToString extends scala.annotation.StaticAnnotation {
inline def apply(defn: Any): Any = meta {
defn match {
case cls @ Defn.Class(_, name, _, ctor, template) =>
val templateStats: Seq[Stat] =
if (template.syntax.contains("toString")) {
template.stats getOrElse Nil
} else {
val toStringMethod: Defn.Def = createToString(name, ctor.paramss)
toStringMethod +: template.stats.getOrElse(Nil)
}
cls.copy(templ = template.copy(stats = Some(templateStats)))
case _ =>
println(defn.structure)
abort("@ToString must annotate a class.")
}
}
}
annotate している対象がdefn
として取得出来るので、パターンマッチ等で必要な情報を取り出す。
元々のクラス定義にtoString
メソッドが実装されている場合は何もしないようにする。
定義されていない場合には、クラス名とパラメータからtoString
メソッドを作成し、
もとの定義に追加した上でそれをクラス定義として上書きするような形。
toString
メソッドを作成するcreateToString
の実装は以下。
def createToString(name: Type.Name, paramss: Seq[Seq[Term.Param]]): Defn.Def = {
val args: Seq[String] = paramss.flatMap { params: Seq[Param] =>
params.map { param: Param =>
val paramName = s""""${param.name}""""
val value = s"${param.name}.toString".parse[Term].get
s"""$paramName + ": " + $value"""
}
}
val joinedParamStrings: Term = args.mkString(""" + ", " + """).parse[Term].get
q"""
override def toString: String = {
${name.syntax} + "(" + $joinedParamStrings + ")"
}
"""
}
クラスのコンストラクタを一つずつ値と型を:
でくっつけ、全体を,
でmkString
し、parse[Term]
でTerm
型として扱う。
クラス名は.syntax
で文字列として取得し、先程のTerm
を()
の中に入れて出力したい文字列とする。
それをoverrider def toString
の実装として渡す。
準クオート(q
)と StringContext による interpolation(s
)を混ぜて使用することが出来ず、
このような実装になってしまった。
実装全体はこちら。
scalameta-prac/ToString.scala
使ってみる
適当な class に@ToString
でアノテーションしてみる。
@ToString
class ToStringClassA(n: Int, label: String)
マクロを展開すると以下のようになり、うまくいっていることが分かる。
class ToStringClassA(n: Int, label: String) {
override def toString: String = {
"ToStringClassA" + "(" + ("n" + ": " + n.toString + ", " + "label" + ": " + label.toString) + ")"
}
}
なぜTerm
を使うのか
実装ではparse[Term].get
を呼んでる箇所があるが、ぱっと見String
のままでも良さそうに見える。
しかし、うまくTerm
を渡さないと意図しない形になってしまうので注意。
例えば以下のようにTerm
だった箇所をString
にすると、
// val joinedParamStrings: Term = args.mkString(""" + ", " + """).parse[Term].get
val joinedParamStrings: String = args.mkString(""" + ", " + """)
生成されるtoString
は以下のように、n.toString
などがエスケープされた文字列になってしまっている。
class ToStringClassA(n: Int, label: String) {
override def toString: String = {
"ToStringClassA" + "(" + "\"n\" + \": \" + n.toString + \", \" + \"label\" + \": \" + label.toString" + ")"
}
}
from: https://qiita.com/petitviolet/items/c18bbda8c431711db462