petitviolet blog

    メタプログラミング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