macroとdynamicを使って任意の型を簡潔に記述、生成する手法がやばい

shapelessの話をします。


versionは2014-12-22現在のmasterです。まだリリース版には入ってないので、色々変わるかもしれません。リリースはされました。

最近のshapelessは、ScalaなのだけどScalaではない何か別のものに進化していってる感があります。今までScalaやっていて見てきたテクニックのなかで、1、2位を争う変態的なものの予感がします。


さて、直接shapelessの話をする前に、まず、これまたほぼ誰も知らないであろう幻のtype macroというのものの話をします。


一時的、macro paradiceに超実験的な機能として入ったのですが、すぐに消えました。
自分が知ってる限り、okomokさんのsignが一時期それ使って実験してました。以下のような感じです。

https://github.com/okomok/sing/blob/95357c2cdd37b2f655bd8b64452e8c0167402a72/core/src/test/scala/BinaryLiteralTest.scala#L40-L43

  val bs: bs = binary("000101")
  type bs     = binary("000101")
  assertSame(bs, _1B D_:: _0B D_:: _1B D_:: DNil)
  assertSame[bs, _1B D_:: _0B D_:: _1B D_:: DNil]

type のところの右側がStringの引数をとっていますね?こんな書き方現状のScalaではできません。type macroは先送りとかではなく基本的にボツになったはずなので、近い将来に入ることもおそらくないでしょう。
上記のStringはコンパイル時に評価されるものでした。引数に渡すStringによって、コンパイル時に型が決定されます。言い換えると
コンパイル時にStringリテラルを受け取って、型を返す関数」
とでもいえばいいでしょうか。
上記の例は型レベル自然数です。しかし、よくあるペアノ数ではなく、別の手法です。詳しく知りたい人はこれ
https://apocalisp.wordpress.com/2010/06/24/type-level-programming-in-scala-part-5a-binary-numbers/
とか読んでください。*1

さて、そんなtype macroはあくまでも幻で、ボツになったものです。okomokさんもこれに関して

http://mbps.hatenablog.com/entry/2014/05/12/155226

Type macroがなくなった。
言語の見た目を拡張するのはよろしくない、という方針か。
どうエミュレートするか悩みどころ

と言っています。


さて、なぜshapelessの話をするといったのに、type macroの話をしたのか?というと、このtype macroの代替となりそうな手法が、shapelessのmasterに入ったからです。
完全な代替となるのか?特定のパターンのみだけなのか?はまだよくわかりません。

論よりコード?ということで、まずは使用例を見てみましょう。

https://github.com/milessabin/shapeless/blob/3249ad2230b86e23d135d84a30b17efa503a68c2/core/src/test/scala/shapeless/unions.scala#L39

type U = Union.`'i -> Int, 's -> String, 'b -> Boolean`.T

なんでしょうかこれ・・・?ぱっと見なにやってるのか意味不明ですね。
まず、Unionというおそらくobjectに対して、ドットの後に `'i -> Int, 's -> String, 'b -> Boolean` となっていますが、普通にScalaシンタックス的には、メソッドかフィールドかタイプメンバーとかのはずですね?*2

しかし、もちろんUnionのオブジェクトには、`'i -> Int, 's -> String, 'b -> Boolean` などという奇怪な名前のものが直接定義されているわけではありません。これがつまり「dynamicとmacro」によって実現されています。

関連するUnionの定義は以下のようになっています。

https://github.com/milessabin/shapeless/blob/3249ad2230b86e23d135d84a30b17efa503a68c2/core/src/main/scala/shapeless/unions.scala#L53-L57

object Union extends Dynamic {
  // 中略

  def selectDynamic(tpeSelector: String): Any = macro LabelledMacros.unionTypeImpl
}

なるほど・・・?まずDynamic知らない人は、適当にググってください。超簡単に説明しておくと、メソッド名がStringとして引数に渡ってくるだけです。

そういえば、dynamicとmacroの組み合わせと言えば、せらさんが発明して?scalikejdbcにも入っていますね。

Type Dynamic を type safe に扱う方法

ある意味、基本的な発想はscalikejdbcのものと同じです。
ポイントはshapelessがなにをしているかというと、

  • whiteboxマクロを使って、コンパイル時に、selectDynamicにわたってきたメソッド名を元に、なんらかのtype memberであるTを保持するオブジェクトを返す
  • そのオブジェクトはコンパイル時にマクロで生成する
  • そのオブジェクトは、type member参照のためにしか使われない

whiteboxマクロは、コンパイル時に頑張れば、任意の型を返せます。shapelessがなぜこれを提供したのか?というと、singleton typeを含んだRecordなどの型を直接書こうとすると、SIP-23 がまだ実装されていない、などの様々な要因により、とても長くなってしまうからでしょう。


さて、shapelessは現在Recoedなどの一部に関してのみこの機能を使ったものを提供しているようですが、これを応用すればなんでもできる気がしませんか?


というか、自分の理解が間違ってなければ「コンパイル時にStringを受け取って」「任意の型を返す」というのは、少し記法が違えども、幻のtype macroとほぼ同じもののような気がします?*3
だとすれば、このテクニックを使えば、signがmacro paradiceを使ってやろうとしていた(?)
「型の定義と値の定義をできるだけ同じ見た目で書く」
が実現できる気がします。


なんでもできるとは、つまり

type A = TypeMacro.` 型をコンパイル時計算するための任意のDSL `.T

とかできるのでは?という意味です。夢が広がりませんか?最初にも書きましたが、本当ここまでやりだすと、もはやScalaではない別のなにかな感はありますが・・・。


これが入ったshapelessの次のversionはやく出ないかなー。もう出ました

あとは、このテクニック使って、誰か変態的なもの作ってみてください

*1:ペアノ数かどうか?ペアノ数以外の実装はどういうものなのか?は、今回の話とは基本的に関係ないです

*2: ` で囲うと、スペースが入っていたり、普段使えない文字や予約語だったとしても、それ全体が一つの識別子としてくれる機能がScalaにはあります

*3:このテクニックの場合、dynamicを使ったメソッド名として任意のStringを渡し、なおかつ返ってきたtype memberを参照することにより、1行で簡潔に型の定義ができるのがポイントです