shapeless.Genericをmacro annoationでcompanion objectに自動生成するライブラリ作った

これ ↓ 作りました。

https://github.com/xuwei-k/shapeless-annotation

以下の記事の続きでもあります

xuwei-k.hatenablog.com

上記のcompiler pluginにおいて、導出が多い型が判明した場合に、対策として一番単純というかわかりやすいのは、companion objectに明示的にGenericのinstanceを定義してしまうことです。

shapeless.Genericに限らないScalaの一般的な仕様として、companionにimplicitやgivenで定義すると、自動で探索のスコープに入る、というものがあります。

よって、そこに定義してしまえば、原理上は1回だけで済むはずです。 Scala 3のMirrorの実態も以前companionに直接compilerによって追加されていることを書いたので、余計な外部のshapeless依存追加の気持ち悪さ、という点を除けば、companionに定義してしまうのは、ある意味では自然であると言える・・・のか?

xuwei-k.hatenablog.com

もし、数百回や数千回導出されていたら、手動定義も仕方ないかぁ、となるかもしれませんが、コンパニオンの手動定義は以下のような微妙な点がいろいろあります

  • Scala 3でそのまま書けない可能性
  • Auxの方の型で明示する場合は、結局case classのfieldの型を順番に全部書く必要があり、それはすごく面倒
    • 特にfield数が多い場合や、増減が激しいclassの場合
    • implicitの型はScala 2.13でも最新で -Xsource:3-cross など指定すると、明示する必要があるので

よって

だったらmacro annotationで自動生成にすれば、それらのデメリットが割と回避可能では!!!

というのが作った動機です。

本当は

「compiler pluginでほぼ全てのcase classに全自動で付与すればいいのでは???」

という構想もありましたが、compiler pluginでTreeを生成するのは普通のmacroより格段に難しかったので、一旦諦めました。

さて、「Auxの方の型で明示」というのは、例えば

case class A(x: Int, y: String)

の時に

Generic[A]

Generic.Aux[A, Int :: String :: HNil]

の2種類があり得ますが、後者のことです。

作ったといっても、READMEにも貼ったのですが、実装の半分くらいはshapeless本体のテストコード内部に置いてあったので、それを拝借しました。

https://github.com/milessabin/shapeless/commit/df313df8c3efb6a2e9094341bc66e7e2d78edd7d

追加で工夫したことは

  • Scala 3ではmacro annotationではない、単なるマーカー的な何も意味がないannotationを置いておき、Scala 3でcross buildしても困らないようにする
  • Aux の方の型を雑に生成するようにした
    • 普通のcase classしか対応してない?ので、あとでもっと改善したい

などです。 この方法なら、アノテーションを付与するだけでよく、fieldが増減した場合にも手動で変更する必要がないはずです。

また、Scala 3の場合にアノテーションが1つのこのライブラリの依存が増えるだけで、余計なshapeless 2本体の依存が増えることもないし、使う側は、特別なbuild設定や src/main/scala-2src/main/scala-3 で分ける必要もないはずです。

作るだけ作って、まだそんなに実用してないので、思ったより微妙とか、何か問題が見つかる可能性もゼロではないですが、とりあえずこれから使ってみて、何かあればさらに書くかもしれません。

同じことで困ってたら使ってみてください。