Scalaのマクロ使ってコンパイル時に全部Applicaiveをインライン展開するzeroapplyというライブラリを作った

https://github.com/xuwei-k/zeroapply


version 0.1.0 リリースしてsonatype経由でmaven centralに上がってます。READMEのとおりに1行設定すれば使えるはずです。これから書くことは、とくに明記なければversion 0.1.0 についてです。

何をするかというと、このコードを

zeroapply.OptionApply.apply3(Option(1), Option(2), Option(3))(_ + _ + _)

コンパイル時に、マクロで、以下ようなネストされたif文に展開します

{
  val x0: _root_.scala.Option[Int] = scala.Option.apply[Int](1);
  if (x0.isEmpty)
    _root_.scala.None
  else
    {
      val x1: _root_.scala.Option[Int] = scala.Option.apply[Int](2);
      if (x1.isEmpty)
        _root_.scala.None
      else
        {
          val x2: _root_.scala.Option[Int] = scala.Option.apply[Int](3);
          if (x2.isEmpty)
            _root_.scala.None
          else
            _root_.scala.Some(x0.get.+(x1.get).+(x2.get))
        }
    }
}


ポイントは、最後の引数にFunctionをそのまま入力した場合、それもインライン展開されます。
「え、コード量増えるけど大丈夫なの?」と思うかもしれませんが、普通のfor式や、Scalazのどの書き方も全部Functionの無名クラスを大量に生成するので、むしろこれ使うとバイナリサイズは大抵減ると思います。

「なぜネストされたif文に展開するのか?」というと、たとえばOptionの場合、最初がNoneだったら必ず全体の結果もNoneなので、2番目と3番目は評価する必要ないわけです。scalaz.Apply#applyNでは、それらの評価タイミングはFunction0を使って制御していますが、結局Function0が生成されるコストがかかるので微妙です。


それをマクロ使ってインライン展開することにより "簡潔なシンタックス" と "実行時効率" を両立しました。

まぁ実際Function0の生成コストなんて大したこと無いとも思いますが。なので実用性は未知ですが、マクロ使ってこんなことできるなーと思いついたので、まずはやってみたかっただけ、という理由も大きいです。

そのほかメリット、デメリット、現時点の注意事項などをまとめておくと

  • マクロは2.10とのクロスビルドも面倒なので、Scala2.11でしかビルドしてない
  • scalaz7.1.xの一部のclassにも対応
    • scalazのほうはモジュール分けて、別のjarにしてある
    • これ書いた現在、scalaz7.0.x向けにはビルドしてない
  • 現在対応してるのは、scala.Option, scala.Either, scalazのEither, scalaz.LazyEither, scalaz.LazyOption, scalaz.Maybe
    • 今後、その他のclassも対応するかもしれないが、どれをやるかは未定
    • 対応してもほとんど意味ないclassも多数ある(第一引数の結果に関わらず、第二引数を必ず評価するパターン)ので、すべてに対応するわけではない
  • メソッド名やシグネチャは、tuple2, tuple3, tuple4・・・、 apply2, apply3, apply4・・・など、scalazのApplyとほぼ同じ
  • 今のところ、22まで生成(別に、applyNのほうはもっと生成できるけどやってない)
  • scalazのものと比べたら、抽象度は下がってる。scalazのApplyのインスタンスから自動導出してるわけではなく、細かい部分も効率よくなるように*1手作業・・・*2
  • エラーの際のデバックが、慣れてないと面倒かも
  • もしバグがあるか、マクロにした都合上、型推論がscalaz.Applyと稀に異なるとかありえる・・・?
  • 同じコンパイル単位だとマクロ呼べないとか、マクロの実装を参照するにはobjectもしくはmacro bundle形式以外は認められない、という制限つらい・・・*3
  • もっとマクロのコードを抽象化、共通化したかったけど、型から判別しないと無理で、ちょっとどうするのがベストなのかわからなかったので一旦断念した

*1:たとえばキャストするなど

*2:そもそも自動導出可能なのか謎

*3:単なるライブラリ実装者視点の愚痴で、ライブラリ利用者はあまり関係ない話