社内勉強会の資料をちょっと改変したやつ。
メタプロ & scala.metaの入門編。
メタプログラミングとは
プログラミング技法の一種で、ロジックを直接コーディングするのではなく、あるパターンをもったロジックを生成する高位ロジックによってプログラミングを行う方法、またその高位ロジックを定義する方法のこと。
つまり
プログラムを引数としてプログラムを出力とする関数、みたいなもの。
リフレクションも一種のメタプログラミングで、文字列から実行時のオブジェクトに干渉できる。
マクロはプログラムを自動で生成するための仕組み。
内部DSLもある種のメタプログラミングといえる。
Conclusion: Meta-programming is the ability for a program to reason about itself or to modify itself.
Scalaでメタプログラミング
リフレクション以外に、Scalaでもメタプログラミングする方法は用意されている。
公式(scala-lang.org)にマクロについて書いたページがある。
http://docs.scala-lang.org/ja/overviews/macros/usecases
一覧
さらっと紹介すると以下。
- macro annotation
- annotationをつけたクラスやメソッドのメタ情報、例えば型情報が利用できる
- def macro
- 引数のメタ情報を利用できる
- implicit macro
- implicit引数を自動生成出来る
- type provider
- 型そのものを自動生成
- 内部では構造的部分型を利用
- 型そのものを自動生成
- その他にもいろいろ
- マクロバンドルとか(知らない)
どうやればいい?
メタプログラミング関連で実装が進んでいる、scala.metaというライブラリを使う。
現時点ではmacro annotationにあたることしか出来ないが、今後のメタプログラミングの土台になるらしい。
scala.meta
メタプログラミングのための次世代なライブラリ。
Scala.meta is a clean-room implementation of a metaprogramming toolkit for Scala, designed to be simple, robust and portable. We are striving for scala.meta to become a successor of scala.reflect, the current de facto standard in the Scala ecosystem.
macro annotation?
例えば、@logging
というannotation。
実行前にstart
、実行後に結果とともにend
と出力するように自動で書き換える。
@logging
def double(n: Int): Int = {
n * 2
}
展開すると以下のようになる。
def double(n: Int): Int = {
println("start")
val result = {
n * 2
}
println("end: " + result)
result
}
どうやるか
Scalaのプログラムコードそのものを解析してToken
として扱って、それを書き換えることが出来る。
@ import $ivy.`org.scalameta::scalameta:1.6.0`
@ import scala.meta._
@ val xToken = "private val x: Int = 1L.toInt".tokenize
xToken: Tokenized = private val x: Int = 1L.toInt
@ xToken.get
res3: Tokens = Tokens(, private, , val, , x, :, , Int, , =, , 1L, ., toInt, )
(Ammonite-REPLを使用)
Scalaコードを文字列として受け取ってパースしている。
正直、Token
あんまり使ったこと無い。
QuasiQuote(准クォート)の方がよく出てくる。
@ val valX = q"private val x: Int = 1L.toInt"
valX: Defn.Val = private val x: Int = 1L.toInt
@ val func = q"protected def double(n: Int): Int = n * 2"
func: Defn.Def = protected def double(n: Int): Int = n * 2
@ val cls = q"""class Hoge[T](val value: T) { def show = s"value: $$value" }"""
cls: Defn.Class = class Hoge[T](val value: T) { def show = s"value: $value" }
文字列で表現したコードをオブジェクトとして扱える。
unapply
も出来る。
@ val Defn.Class(mods, name, tparams, ctor, templ) = cls
mods: collection.immutable.Seq[Mod] = List()
name: Type.Name = Hoge
tparams: collection.immutable.Seq[Type.Param] = List(T)
ctor: Ctor.Primary = def this(val value: T)
templ: Template = { def show = s"value: $value" }
で、何が出来る?
macro annotationで何かしらの自動生成や書き換えなど。
作ってみたやつ
petitviolet/scala-acase
classをcase classに無理やり変換するannotation。
サンプル
@logging
def double(n: Int): Int = {
n * 2
}
これがこうなる
def double(n: Int): Int = {
println("start")
val result = {
n * 2
}
println("end: " + result)
result
}
サンプルの実装
@compileTimeOnly("logging not expanded")
class logging extends StaticAnnotation {
inline def apply(defn: Any): Any = meta {
defn match {
case d: Defn.Def =>
val newBody = q"""
println("start")
val result = ${d.body}
println("end: " + result)
result
"""
d.copy(body = newBody)
case _ =>
abort("annotate only function!")
}
}
}
ほぼQuasiQuoteだけで実装している。
実装の基本方針
inline def apply
の引数には、annotationをつけた対象が引数として与えられる。
つまり、annotationしたのがclassならDefn.Class
、defならDefn.Def
、valならDefn.Val
となる。
その引数に対するパターンマッチでannotationした対象を操作していく。
パターンマッチでよく使うのは以下。
scalameta/Trees.scala
注意点
annotationの実装があるファイルと、それを使ったファイルを同時にはコンパイル出来ない。
そのため、annotationの実装があるファイルだけを先にコンパイルしてから、それを使ったファイルをコンパイルすること。
scala.metaを使ってannotationを実装するモジュールと、annotationを使うだけのモジュールを分けておいた方が問題が起きにくい。
また、annotation classにメソッドは実装できないので、コンパニオンオブジェクトなどに実装すること。
あるいはメソッド内メソッドとして実装する。
@trackingを実装する
@tracking
def heavy(n: Int): Int = {
Thread.sleep(100)
n * 2
}
これがこうなる@tracking
を実装する。
def heavy(n: Int): Int = {
val start = System.nanoTime()
val result = {
Thread.sleep(100)
n * 2
}
val end = System.nanoTime()
println(s"[heavy] tracking time: ${(end - start) / 1000000} ms")
}
実装例
QuasiQuoteを使って実装するとこんな感じになる。
inline def apply(defn: Any) = {
val getNano = q"System.nanoTime()"
defn match {
case d @ Defn.Def(_, name, _, _, _, body) =>
val newBody = q"""
val methodName = ${name.value}
val start = $getNano
val result = $body
val end = $getNano
println(s"[$$methodName] tracking time: $${(end - start) / 1000000} ms")
result
"""
d.copy(body = newBody)
case _ =>
abort("annotate only function!")
}
}
頑張って実装する
scala.metaが用意している型を真面目に使って実装するとこんな感じ。
inline def apply(defn: Any) = {
// val $name = System.nanoTime()
def getNano(name: Term.Name) = {
val systemNano = Term.Select(Term.Name("System"), Term.Name("nanoTime"))
val getNanoTime = Term.Apply(systemNano, Seq.empty)
Defn.Val(Seq.empty, Seq(Pat.Var.Term(name)), Option(Type.Name("Long")), getNanoTime)
}
// val start = System.nanoTime()
val startValName = Term.Name("start")
val start = getNano(startValName)
// val end = System.nanoTime()
val endValName = Term.Name("end")
val end = getNano(endValName)
// val $name = $func
def getResult(name: Term.Name, func: Defn.Def): Defn.Val =
Defn.Val(Seq.empty, Seq(Pat.Var.Term(name)), None, func.body)
// println(s"[$methodName] tracking time: ${(end - start) / 1000000} ns")
def printElapsedTime(func: Defn.Def): Term = {
val milli = {
val nano = Term.ApplyInfix(endValName, Term.Name("-"), Seq.empty, Seq(startValName))
val op = Term.Name("/")
val arg = Seq(Lit(100000))
Term.ApplyInfix(nano, op, Nil, arg)
}
val interpolate = Term.Interpolate(
Term.Name("s"),
Seq(Lit(s"[${func.name.value}] tracking time: "), Lit(" ms")),
Seq(milli)
)
Term.Apply(Term.Name("println"), Seq(interpolate))
}
defn match {
case d: Defn.Def =>
val resultName = Term.Name("result")
val newBody =
Term.Block(Seq(
start,
getResult(resultName, d),
end,
printElapsedTime(d),
resultName
))
d.copy(body = newBody)
case _ =>
abort("annotate only function!")
}
}
まとめ
メタプログラミングを使えば、普通では出来ない抽象化や共通化など、メタプログラミングを使えるチャンスは多い。
少し込み入ったことをしているライブラリなどでは頻繁に出てくるのでコードを読めるようになるために不可欠、かも知れない。
公式のチュートリアルもやってみましょう
http://scalameta.org/tutorial/