メタプログラミングScala

  • 9
    いいね
  • 0
    コメント

社内勉強会の資料をちょっと改変したやつ。
メタプロ & scala.metaの入門編。

メタプログラミングとは

プログラミング技法の一種で、ロジックを直接コーディングするのではなく、あるパターンをもったロジックを生成する高位ロジックによってプログラミングを行う方法、またその高位ロジックを定義する方法のこと。

メタプログラミング - wikipedia

つまり

プログラムを引数としてプログラムを出力とする関数、みたいなもの。

リフレクションも一種のメタプログラミングで、文字列から実行時のオブジェクトに干渉できる。

マクロはプログラムを自動で生成するための仕組み。

内部DSLもある種のメタプログラミングといえる。

参考:StackOverFlow

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

メタプログラミングのための次世代なライブラリ。

scalameta.org

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/