blog.petitviolet.net

scalametaでの型パラメータとコンストラクタ

2017-02-19

QiitaScalametaprogrammingmeta

tl;dr

直接、annotation class の型パラメータやコンストラクタにはアクセス出来ない。 その代わりに、thisを用いてパターンマッチで取得することが出来る。

題材

mix-in injection とか minimal cake pattern と呼ばれる、DI 対象となる class を mix-in する macro annotation を実装した。

  • Uses[MyService]とするとval myService: MyServiceがフィールドに追加される
  • MixIn[MyService](new MyServiceImpl)するとval myService: MyService = new MyServiceImplがフィールドに追加される

こんな感じで使用する。

trait MyService { def double(n: Int) = n * 2 }
object MyServiceImpl extends MyService

trait OtherService { def triple(n: Int) = n * 3 }
class OtherServiceImpl extends OtherService

@Uses[MyService]
@Uses[OtherService]
trait UsesFieldTarget {
  def showDouble(n: Int) = println(this.myService.double(n))
  def showTriple(n: Int) = println(this.otherService.triple(n))
}

@MixIn[MyService](MyServiceImpl)
@MixIn[OtherService](new OtherServiceImpl)
class UsesFieldTargetImpl extends UsesFieldTarget

object UsesFieldApp extends App {
  val impl = new UsesFieldTargetImpl
  impl.showDouble(100) // 200
  impl.showTriple(100) // 300
}

annotation の実装はscalameta-prac/uses.scalaで、 サンプルコードはscalameta-prac/UsesApp.scala

ちなみに、IntelliJ 上ではうまく解決出来ておらずMacro expansion failedとか表示されるし、補完もされない…。 こんな感じ。。

スクリーンショット

このような話もあるので期待したい。 IntelliJ IDEA 2016.3 RC: Scala.Js, Scala.Meta and More | IntelliJ Scala plugin blog

macro annotation で型パラメータと引数を処理する

上のような使い方をするには型パラメータと引数を処理しなければならないが、inline macro の場合はちょっと特殊。

型パラメータを取得する

class Uses[T] extends scala.annotation.StaticAnnotation {
  inline def apply(defn: Any): Any = meta { ??? }
}

このようなUses[T]において、Tには直接アクセス出来ない。 thisを用いるとTTerm.ApplyTypeとして取得できる。

class Uses[T] extends scala.annotation.StaticAnnotation {
  inline def apply(defn: Any): Any = meta {
    val typeParam: String =
      this match {
        case Term.New(Template(_,
            Seq(Term.Apply(
              Term.ApplyType(
                _,
                Seq(Type.Name(name: String))
              ), _)), _, _)) =>
            name
        case _ => None
      }
      ???
  }
}

これで型パラメータとして渡されたTのコンパイル時における型名が文字列として手に入る。 つまりUses[MyService]みたいになっているとMyServiceという文字列が取れる。

コンストラクタを処理する

Uses[T]でねじ込んだT型のフィールドに実装を与えるMixIn[T](t: T)を作る。 そのためにはコンストラクタを手に入れる必要がある。 これも型パラメータと同様にthisに対するパターンマッチで取り出す。

型パラメータとコンストラクタをそれぞれ取り出すパターンマッチは以下のように書ける。

this match {
  case Term.New(Template(_, Seq(
    Term.Apply(
      Term.ApplyType(_, Seq(Type.Name(name: String))),
      Seq(implArg: Term.Arg)
    )
  ), _, _)) =>
    (name, implArg)

Term.Applyに与えられる引数のTerm.ApplyTypeSeq[Type.Arg]として型パラメータとコンストラクタが得られる。 これでMixIn[T](t: T)Ttが無事に得られる。

ちなみに、引数を渡すから型パラメータは型推論で解決してくれるだろう、と思ってもダメ。

Error:wrong number of type arguments for MixIn, should be 1

みたいな型パラメータが足りませんよ、というエラーが出てしまう。

from: https://qiita.com/petitviolet/items/c751b52df53b0f07da4c