petitviolet blog

    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