メタプログラミングScala
2017-04-18
QiitaScalametaprogramming社内勉強会の資料をちょっと改変したやつ。 メタプロ & 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/
from: https://qiita.com/petitviolet/items/1dadf81ff2f4b04335d9