scalametaに入門する


scalametaはinline macroと呼ばれるmacro annotationのためのツールキット。
型安全にmacro出来る。

公式とチュートリアルは以下。

- [scala.meta](http://scalameta.org/)
- [A Whirlwind Tour of scala.meta](http://scalameta.org/tutorial/#Part4-Macroannotations)

# setup

まずは使えるようにするためにbuild.sbtに追記。

```sbt
libraryDependencies += "org.scalameta" %% "scalameta" % "1.5.0"
```

ちなみに[Ammonite-REPL](http://www.lihaoyi.com/Ammonite/#Ammonite-REPL)を使っているなら以下。

```
import $ivy.`org.scalameta::scalameta:1.5.0`
```

### import

importはこう。
とりあえずワイルドカードでimportしてしまう。

```
import scala.meta._
```

以下のサンプルでは省略している。

# scalametaの基礎概念

## Tokens

Scalaの文を切り分けたもの。
metaの基本になる。
とりあえず`xxx.syntax`としておけば、そのtokenを表現する文字列が手に入ることをおぼえておけば便利。

```java
@ "val x = 1".tokenize
res7: Tokenized = val x = 1
@ "val x = 1".tokenize.get
res8: Tokens = Tokens(, val,  , x,  , =,  , 1, )
@ "val x = 1".tokenize.get.syntax
res9: String = "val x = 1"
@ "val x = 1".tokenize.get.structure
res10: String = "Tokens(BOF [0..0), val [0..3),   [3..4), x [4..5),   [5..6), = [6..7),   [7..8), 1 [8..9), EOF [9..9))"
@ val tokens = "val x = 1".tokenize.get
tokens: Tokens = Tokens(, val,  , x,  , =,  , 1, )
@ tokens.map { x => f"${x.structure}%10s -> ${x.getClass}" }.mkString("\n")
res13: String = """
BOF [0..0) -> class scala.meta.tokens.Token$BOF
val [0..3) -> class scala.meta.tokens.Token$KwVal
    [3..4) -> class scala.meta.tokens.Token$Space
  x [4..5) -> class scala.meta.tokens.Token$Ident
    [5..6) -> class scala.meta.tokens.Token$Space
  = [6..7) -> class scala.meta.tokens.Token$Equals
    [7..8) -> class scala.meta.tokens.Token$Space
  1 [8..9) -> class scala.meta.tokens.Token$Constant$Int
EOF [9..9) -> class scala.meta.tokens.Token$EOF
"""
@ "val x = \"10".tokenize.get
<input>:1: error: unclosed string literal
val x = "10
        ^
"""
```

## Quasiquotes

準クオート。scalametaというよりmacro全般に共通する。
http://docs.scala-lang.org/overviews/quasiquotes/intro.html
文字列から構文木を生成してくれる  

![Tree](http://scalameta.org/tutorial/img/tree.png)

文法は`q"xxx"`。
string-interpolationと同じように使用できる。

クオートした文字列がどういうものか、`val`なのか`def`なのか`{???}`なのか...、で
型が確定し、クオート内で展開するにも文法が正しくないとコンパイルエラーになる。

ちなみにその型の実装はこの辺で見れる [scalameta/scalameta/Trees.scala](https://github.com/scalameta/scalameta/blob/master/scalameta/trees/src/main/scala/scala/meta/Trees.scala)。

注意点は`scala.Seq`ではなくて`scala.collection.immutable.Seq`を使うこと。
使い方のサンプルはこんな感じ。

```scala
@ val s = collection.immutable.Seq(q"val x = 10", q"def double(value: Int) = value * 2")
res38: Seq[Defn] = List(val x = 10, def double(value: Int) = value * 2)
 
@ q"""..$s"""
res39: Term.Block = {
  val x = 10
  def double(value: Int) = value * 2
}

@ q"""start { ..$s } end"""
res21: Term.Select = start {
  val x = 10
  def double(value: Int) = value * 2
}.end

@ val var10 = q"x = 10"
var10: Term.Assign = x = 10

@ val var20 = q"x = 20"
var20: Term.Assign = x = 20

@ val ifVar = q"if (true) $var10 else $var20"
ifVar: Term.If = if (true) x = 10 else x = 20
```

unapplyで取り出すことも出来る。

```scala
@ val statement = q"val x = if (y > 0) 100 else 0"
statement: Defn.Val = val x = if (y > 0) 100 else 0
@ val q"val $x = if ($cond) $trueResult else $falseResult" = statement
x: Pat = x
cond: Term = y > 0
trueResult: Term = 100
falseResult: Term = 0
```

文字列を`parse`して構文木も取れる。

```scala
@ val statement = "val x = if (y > 0) 100 else 0".parse[Stat].get
statement: Stat = val x = if (y > 0) 100 else 0

@ val traitSource = "trait Hoge { def double(n: Int) = n * 2 }".parse[Source].get
traitSource: Source = trait Hoge { def double(n: Int) = n * 2 }
@ val source"trait $traitName { ..$body }" = traitSource
traitName: Type.Name = Hoge
body: collection.immutable.Seq[Stat] = List(def double(n: Int) = n * 2)
```

ここで`source""`という準クオートが出てきているが、他にもたくさんあり、[scalameta/Api.scala](https://github.com/scalameta/scalameta/blob/master/scalameta/quasiquotes/src/main/scala/scala/meta/quasiquotes/Api.scala)に実装がある。

などなど色々あるが、tokenと準クオート(`q""`)を抑えておけば、とりあえず手を動かせる。

# macro annotationを実装してみる

scalametaでMacro Annotationを実装する。
[A Whirlwind Tour of scala.meta](http://scalameta.org/tutorial/#Part4-Macroannotations)

普通のclassだけど`toString`はcase classのようにしたい、というのはよくあるはず。
JavaならLombokで[[@ToString](https://projectlombok.org/features/ToString.html)]がある。
これに近いことをscalametaでやってみる。

## 実装

`scala.annotation.StaticAnnotation`をextendsしたclassを定義する。
クラスの本体は`inline def apply(defn: Any): Any = meta { ??? }`が大体テンプレ。
内部で使う`Seq`は`collection.immutable.Seq`なのでimportしておくこと。
全体像は以下。

```scala
import scala.meta._
import scala.collection.immutable.Seq

class ToString extends scala.annotation.StaticAnnotation {
  inline def apply(defn: Any): Any = meta {
    defn match {
      case cls @ Defn.Class(_, name, _, ctor, template) =>
        val templateStats: Seq[Stat] =
          if (template.syntax.contains("toString")) {
            template.stats getOrElse Nil
          } else {
            val toStringMethod: Defn.Def = createToString(name, ctor.paramss)
            toStringMethod +: template.stats.getOrElse(Nil)
          }

        cls.copy(templ = template.copy(stats = Some(templateStats)))
      case _ =>
        println(defn.structure)
        abort("@ToString must annotate a class.")
    }
  }
}
```

annotateしている対象が`defn`として取得出来るので、パターンマッチ等で必要な情報を取り出す。
元々のクラス定義に`toString`メソッドが実装されている場合は何もしないようにする。

定義されていない場合には、クラス名とパラメータから`toString`メソッドを作成し、
もとの定義に追加した上でそれをクラス定義として上書きするような形。

`toString`メソッドを作成する`createToString`の実装は以下。

```scala
    def createToString(name: Type.Name, paramss: Seq[Seq[Term.Param]]): Defn.Def = {
      val args: Seq[String] = paramss.flatMap { params: Seq[Param] =>
        params.map { param: Param =>
          val paramName = s""""${param.name}""""
          val value = s"${param.name}.toString".parse[Term].get
          s"""$paramName + ": " + $value"""
        }
      }
      val joinedParamStrings: Term = args.mkString(""" + ", " + """).parse[Term].get

      q"""
       override def toString: String = {
         ${name.syntax} + "(" + $joinedParamStrings + ")"
       }
      """
    }
```

クラスのコンストラクタを一つずつ値と型を`: `でくっつけ、全体を`, `で`mkString`し、`parse[Term]`で`Term`型として扱う。
クラス名は`.syntax`で文字列として取得し、先程の`Term`を`()`の中に入れて出力したい文字列とする。
それを`overrider def toString`の実装として渡す。

準クオート(`q`)とStringContextによるinterpolation(`s`)を混ぜて使用することが出来ず、
このような実装になってしまった。

実装全体はこちら。  
[scalameta-prac/ToString.scala](https://github.com/petitviolet/scalameta-prac/blob/4a2b323f3620519c6e4af298cccbe42b87a3b94b/modules/metas/src/main/scala/net/petitviolet/metas/ToString.scala)

### 使ってみる

適当なclassに`@ToString`でアノテーションしてみる。

```scala
@ToString
class ToStringClassA(n: Int, label: String)
```

マクロを展開すると以下のようになり、うまくいっていることが分かる。

```scala
class ToStringClassA(n: Int, label: String) {
  override def toString: String = {
    "ToStringClassA" + "(" + ("n" + ": " + n.toString + ", " + "label" + ": " + label.toString) + ")"
  }
}
```

### なぜ`Term`を使うのか

実装では`parse[Term].get`を呼んでる箇所があるが、ぱっと見`String`のままでも良さそうに見える。
しかし、うまく`Term`を渡さないと意図しない形になってしまうので注意。

例えば以下のように`Term`だった箇所を`String`にすると、

```scala
// val joinedParamStrings: Term = args.mkString(""" + ", " + """).parse[Term].get
val joinedParamStrings: String = args.mkString(""" + ", " + """)
```

生成される`toString`は以下のように、`n.toString`などがエスケープされた文字列になってしまっている。

```scala
class ToStringClassA(n: Int, label: String) {
  override def toString: String = {
    "ToStringClassA" + "(" + "\"n\" + \": \" + n.toString + \", \" + \"label\" + \": \" + label.toString" + ")"
  }
}
```