Type Provider によるコンパイル時プログラミング

ハワイにいる間、魔導書の書評以外にもちゃんとハワイらしいことしてきたという報告です(嘘
F# 3.0 で使えるようになる予定の Type Provider ですが、これをさっそく使ってみました。
今回のプログラムは Visual Studio 11 Developer Preview を使用していますので、実際に動かしてみたい人はインストールしてください。

Type Provider って?

まず、Type Provider って何なのよ、ということについてです。
Type Provider というのは、

  • コンパイル時に(プリプロセスとかではない・・・と思う)
  • コード生成に頼らずに
  • 型を生成する仕組み

のことだと理解しています。

F# 3.0 Information Rich Programming(PDF) を見る限りの TypeProvider の一番の目的は、「コード生成によらない型の生成」にあるように思えます。
どういうことかというと、今までは

  • リソースにアクセスするためにデザイナをいじって、VS が裏でコードを生成する
  • DB とやりとりするためにデザイナをいじって、VS が裏でコードを生成する (DataSet / Linq to SQL / Entity Framework / ...)
  • Web サービスを使うために定型的なコードを書きまくる。もしくは T4 などでお茶を濁す
  • WPF などで MVVM パターンを適用するために ViewModel などで定型的なコードを書きまくる。もしくは T4 などでお茶を濁す

などなど、コード生成 (あとはリフレクション?) に頼っていた場面というのは結構あります。
Type Provider はこの問題を解決することに重点を置いているようです。
例えば、DB とのやり取りが行いたければ、

type SQL = SqlConnection<"Server=...">

このように 1 行書くだけで DB のスキーマから型を生成してくれるようになります。
今まで型名を書いていたところに、接続文字列を書いています。
どうやら、ここに書いた情報をもとにして Type Provider がコンパイル時に型を生成してくれるようです。

型の代わりに値が書けることによって広がる世界

ここからがこのエントリの本題です。
Type Provider を導入するにあたり、ジェネリックのパラメータとして値を入れることができるようになったようなのです。
・・・そう、C++ のテンプレートと同じように。


ということはですよ、F# でもできるようになるってことじゃないですかね、コンパイル時プログラミングが。

やってみた

できそうな気はする・・・でもやってみないと分からない!
ということで、やってみました。
独自の Type Provider を定義するのにはいくつか方法があるようですが、今回は F# の文字列をコンパイルして型を作る方法でやりました。
これだと T4 とあまり変わらないような気もしなくもないですが、生成されるコードは表に全く出てこないのでちょっとだけいい感じです。
Type を生成する方法がもっと気軽に使えるようになれば、もっと手軽に、もっと綺麗に Type Provider が使えるようになるかもしれません。


コード自体は github に上げてあるので、興味のある方はどうぞ。
bleis-tift/TypeProviderSample · GitHub
TP というソリューションが本体で、TPClient はサンプルになっています。


このコードを書くに当たり、以下の情報を参考にしました。

  1. API Only - Stack Exchange
  2. First example of a very simple type provider
  3. Mindscape Blog » Blog Archive » F# type providers – as if by magic…

実装方針としては一番下のものと同じです。

作った TypeProvider

作ったのは、

  • 型安全な split
  • 型安全な regex

の 2 つです。
が、似たような部分はかなりあるので、これをまとめたライブラリ的なものも作りました。

SimpleTypeProvider

独自の TypeProvider で何ができるの?ということが知りたい方は、ここは飛ばして型安全な split へどうぞ。


SimpleTypeProvider は独自の TypeProvider を作るうえで面倒な作業を肩代わりしてくれます。
SimpleTypeProvider.fs には

  • StaticParameter
  • StaticArgument
  • SimpleTypeProvider

の 3 つの module から構成されていて、SimpleTypeProvider module はさらに

  • Info レコード
  • SimpleTypeProviderBase クラス

から構成されます。
重要なのはこの 2 つの型SimpleTypeProviderBase です。


独自の TypeProvider は SimpleTypeProvider を継承し、そのコンストラクタとして Info レコードの値を渡します。

(* 実装を変更しました。
type A = class end

[<TypeProvider>]
type AProvider() =
  inherit SimpleTypeProviderBase begin
    { NameSpace = "適当な名前空間"
      ProvideType = typeof<A>
      StaticParams = [ StaticParameter.make "static parameterの名前" typeof<static parameterの型>; ... ]
      OpenModules = [ "System" ]
      GenSrc = コード生成関数 }
  end
*)
type A = class end

[<TypeProvider>]
type AProvider() =
  inherit SimpleTypeProviderBase<A> begin
    NameSpace = "適当な名前空間",
    StaticParams = [ StaticParameter.make<static parameterの型> "static parameterの名前"; ... ],
    (* 自動実装プロパティを使う用にしたので、デフォルト値でいい場合は省略可能
    OpenModules = [ "System" ] *)
  end

  override this.GenSrc args = コード生成関数の実装

これだけで、独自の Type Provider を作ることができます。
ここで、static parameter というのがジェネリックのカッコの中に書くパラメータになっており、StaticParameter.make の第一引数に指定した文字列は VS のインテリセンスに表示されたりします。
コード生成関数は、StaticArgument.t list を受け取って、文字列として記述した関数を返す関数を指定します。
StaticParameter.t ではなく StaticArgument.t になっているのは、この関数が実行されるときにはすでに引数の値が渡ってきており、それを保持しているからです。

型安全な split

文字列を split するのはよくあることです。
そして、分割後の要素数がコンパイル前に決まっているという状況も少なからずあるでしょう。
しかし、.NET Framework の Split メソッドは汎用的である必要があるため、配列で返ってきます。
これにパターンマッチをかけると、パターンマッチが完全ではないという警告を無視するか、

let a, b =
  match postCode.Split([| "-" |], 2, StringSplitOptions.None) with
  | [| a; b |] -> (a, b)
  | _ -> failwith "oops!"

のように本来不要なケースを記述する必要があります。


これを (コード生成に頼らずに) 解決するために Type Provider を使いました。
型安全な split (TSSplit) を使うと、コードはこのようになります。

[<Generate>]
type PostCodeSplitter = TSSplit<2>

// ...

let a, b = postCode |> PostCodeSplitter.split "-"
型安全な regex

正規表現も split 同様、よく使うと思います(パーサコンビネータを使え、という話は置いておくとして)。
しかし、正規表現は (Boost.Xpressive などの例は置いておいて) 実行時に解析されます。
そのため、せっかく静的型付けの言語を使っているにもかかわらず、正規表現の文法を間違えていた場合は実行時エラーとなってしまいます。


これを (変態的な記述をせずに) 解決するために Type Provider を使いました。
型安全な regex (TSRegex) の基本的な使い方は以下の通りです。

[<Generate>]
type CellPhoneNumberPat = TSRegex< @"(\d{3})-(\d{4})-(\d{4})" >

// ...

let b, c =
  match str |> CellPhoneNumberPat.match' with
  | Some(_, b, c) -> b, c
  | None -> "", ""

この例で、例えば正規表現を間違えたとします。
すると、type CellPhoneNumberPat... の行でコンパイルエラーになります。
しかも、エラーメッセージは例外オブジェクトの Message が表示されるので、非常にわかりやすいです。
似たような例が以下のエントリにあるので、参考にどうぞ。

F# 3.0 Type Provider 正規表現のCompile-time構文チェック - 無料でプログラミングを教えます日記


さらに、TSRegex ではグループを静的に解釈し、match' の戻り値の型が変わるようになっています。
これは TSSplit と同じような感じですが、より実用的な例になっていると思います。


最新版では、名前付きのグループにも対応しました。

[<Generate>]
type UrlPat = TSRegex< @"(?<Scheme>[^:]+)://(?<Host>[^/]+)(?<Path>/.*)" >

let extractPath url =
  let result = UrlPat.nameMatch url
  result.Path.Value

名前なしのグループは _1 のような名前でアクセスできます。

罠的な何か

色々と引っかかった部分があるので、そのあたりの説明です。

アセンブリの生成

TP/CompiledType.fs の compile 関数でコードをコンパイルして、その中の Type を取り出しているんですが、最初はアセンブリをメモリ内に生成しようとしていました。
が、うまく動かないのでデバッガで追って見たのですが、全然例外出ないし、ウォッチを使うと型の生成にも取り出しにも関数呼び出しにも成功しちゃうし、わけわかりませんでした。
エラーメッセージは

バイナリ ファイル '<不明>' を開くときにエラーが発生しました

です。
ファイル生成してないのに開こうとしていたので、インメモリをあきらめて一時ファイルにするとうまくいきました。
求むインメモリでやる方法!

関数の定義方法

現在、実際に呼び出される関数の定義はこんな感じになっています。

let split =
  let split' (sep: string) (str: string) =
    ...
  split'

最初はもっとシンプルに、

let split (sep: string) (str: string) =
  ...

としてみたのですが、これだとコード上はカリー化されているにも関わらず、呼び出すときにはタプル形式になってしまいました。
なので、FSharpFunc を使ってくれるように回りくどい書き方になっています。

dll がつかみっぱなしになってしまう

Type Provider の dll は扱いが割と面倒で、github に上げたような構成にすると dll をどいつかがつかみっぱなしになってしまって、よくコンパイルができない状況に陥ります。
そのため、開発中はソリューションを分け、

  • 開始動作を「外部プログラムの開始」にして VS 11 の exe を指定する (C:\Program Files (x86)\Microsoft Visual Studio 11.0\Common7\IDE\devenv.exe とかそんな感じ)
  • コマンドライン引数に Type Provider の dll を使うソリューションを絶対パスで指定する

という方法を使っていました。
ここらへんも、何かもっといい方法があるなら教えてほしいです。

ということで

Type Provider を使うことで、F# でもコンパイル時に色々とできるようになりました。
現状では情報も少なく、かつ API の整備もされていないような状態なので、泥臭い感じになってしまっています。
それに、最初に書いたように Type Provider の主眼はコンパイル時プログラミングには置かれていないようです。


でも、ここら辺は時間がたつにつれて解決されていくと楽観しています。
少なくとも、自分の中では T4 は完全にいらない子になりました(ただし F# に限る)。
Type Provider によって F# に新たな世界が開けたことはまず間違いないので、ぜひ皆さんも Type Provider によるコンパイル時プログラミングを楽しんでください!