blog.petitviolet.net

メタプログラミングScala

2017-04-18

QiitaScalametaprogramming

社内勉強会の資料をちょっと改変したやつ。 メタプロ & 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/

from: https://qiita.com/petitviolet/items/1dadf81ff2f4b04335d9