読者です 読者をやめる 読者になる 読者になる
ようこそ。睡眠不足なプログラマのチラ裏です。

型プロバイダー(TypeProvider)のちょっとしたアレコレ

F# F#3.0 型プロバイダー TypeProvider BulletML DSL

一応、型プロバイダー(TypeProvider)のとりとめもない話 の続き。 ちょっと草植えときますね型言語Grass型プロバイダーを作った後、少し思い違いをしていた事に気付いたのが事の発端。この記事では、FsBulletML.TypeProvidersを作成する過程で得た、型プロバイダーについてのちょっとしたアレこれについて書いてみる。

オレは型プロバイダーに対する思い違いをしていた(雑魚)

  FsBulletMLという弾幕記述言語ライブラリを作っています。このライブラリでは、弾幕記述言語BulletML(XML形式等)を読み込んで弾幕を表現する判別共用体(内部DSL)の型を生成するということをやっています。構想の段階で BulletML(XML形式等) に判別共用体(内部DSL)の型を付ける型プロバイダーの提供も考えていたが、型プロバイダーでは判別共用体を作ることができないという理由から、このライブラリでの型プロバイダーの提供は一時保留としていた。


  で、
 

 


  大きな思い違いをしていたと気が付いた。型プロバイダーによってメタデータを元に"判別共用体の型そのものは作ることはできない"が、"型プロバイダーで作成した型が持つメソッドやプロパティを通じて、メタデータを元に作成した判別共用体を返すことはできる"ということに(気付くの遅すぎ)。
   

FsBulletMLで型プロバイダーを提供してみよう

ということで、FsBulletMLで型プロバイダーを提供してみることにした。

FsBulletMLで提供している3つの外部DSL(XML形式、SXML形式、FSB形式)をBulletml判別共用体(内部DSL)に型付けしてくれる型プロバイダーを提供したい。FsBulletMLでは、もともとFsBulletML.CoreFsBulletML.Parserという2つのパッケージをNuGetで提供している。これに加えて、FsBulletML.TypeProvidersというパッケージを新たに作成して提供したい。


FsBulletML.TypeProvidersを作成する過程で学んだことについて書いてみる。以下で現れる TypeProviderForNamespaces クラスは、FSharp.TypeProviders.StarterPack を利用している。


 

型プロバイダーに渡すことができる静的引数の種類

静的引数を扱う型プロバイダーについて、メタデータとしてもっとも渡されることの多い静的引数は string(文字列型) だろう。実際、文字列さえ渡すことができればだいたいなんでもできる。ただ、他にどのような型を渡すことができるのかについても把握しておきたい。


基本的には、プリミティブ型 (F#)を静的引数として渡すことができる。ただし、nativeint, unativeint, System.Void, unit など CLI定数リテラルとしてコンパイルできないものは型プロバイダーの静的引数として渡すことができない。また、enum(列挙型) も基になる型は (sbyte、byte、int16、uint16、int32、uint32、int64、uint64、char) のいずれかであるため、静的引数として渡すことができる。


 


  実行結果

 

 

へぇ。とくに面白くはない。


型プロバイダーの実行部分は部分的な制限がある

型プロバイダーでは、メソッドやプロパティの実装をコードクォートによって行うため部分的な制限がある。

たとえば、

Valueプロパティの値を、XMLドキュメントでも参照できるようにしている。上記のコードは、コンパイルが通るので一見良さそうにみえるが、コードクォート内から value に束縛した値をうまく参照することができないため、以下のようなエラーとなる

f:id:zecl:20140825175254p:plain


なんてことはない。直接コードクォート内に記述するとうまくいく。

ただこれだとコードが重複してしまって気持ちが悪い。


ひとつは、以下のようにFSharp.PowerPackを用いてコードの重複を避けるという方法も考えられるが、Linq.QuotationEvaluationは万能とはいいがたいし、 FSharp.PowerPackに依存するのも何か違う感じがするのでこれは避けたい。

 

通常は、module に関数を外出しすることでこれを回避する。

  これはコンパイルも通るし Sample4 型プロバイダーを利用する側のコードもコンパイルが通るので良さそうに見える。  

f:id:zecl:20140825180549p:plain


  しかし、実行すると次の例外が発生する。当然と言えば当然だが型プロバイダーの実行時に public ではない module を参照することはできないからである。  

f:id:zecl:20140825180421p:plain

 

かといって、 Hogehoge module を単に public にするだけだと、見せたくないものがそのまま垂れ流しで見えてしまうので、どうも具合がわるい。そこで、CompilerMessage属性を利用するという苦肉の策を使う。

 
ところで、EditorBrowsable氏~ 人気ないの~?ないの~?
F# IntelliSense doesn't respect the EditorBrowsable attribute


他のDLLに依存する型プロバイダーを作る

FsBulletMLで型プロバイダーを提供するにあたって、実装はFsBulletML.CoreおよびFsBulletML.Parserに依存するようにしたい。 FsBulletML.Coreは、XML形式のBulletMLをパースする機能を持っている。FsBulletML.Parserは、SXML形式とFSB形式をパースする機能を持っている。 それぞれのDLLを参照してしまえば、ちょっと草植えておきますね型言語Grass型プロバイダーと同じようなノリで簡単に実装できるはずだ。そう考えた。 実際、実装そのものは容易にできた。しかし実行するとうまくいかない。単に依存対象のDLLを参照しただけではだめなのだ。型プロバイダーのコンパイルは通るが、利用時にエラーとなる。型プロバイダーのコンパイル時に参照できている DLL が実行時には参照できないことが原因だ。


  たとえば、次のコードの DLL を参照した型プロバイダーを作る。


型プロバイダー

コンパイルが通るし一見良さそうに見えるが、この型プロバイダーを利用しようとすると以下のようになる。

f:id:zecl:20140825181323p:plain

型プロバイダーのコンパイル時に参照できている DLL が型プロバイダーの実行時に参照できていないためにこのようなエラーとなる。 Library1.dll が存在するパスを、型プロバイダーの探索対象にあらかじめ登録しておく必要がある。 TypeProviderForNamespaces クラスのRegisterProbingFolderメソッドでこれを解決できる。

このように実装することで、型プロバイダーと同じパスに存在するDLLが実行時に参照可能となる。


他のNuGetパッケージに依存した型プロバイダーを作ってNuGetで配布するときのやり方

FsBulletML.TypeProvidersは、FsBulletML.CoreおよびFsBulletML.ParserのNuGetパッケージに依存するかたちで配布したい。この場合、他のNuGetパッケージが展開されるフォルダのパスを考慮した実装が必要となる。もっとも良さそうな方法は、package.configのXMLを読み込んで参照するパスを解決する方法が考えられる。 FsBulletML.TypeProvidersでは、下記のような感じで、型プロバイダーが依存するNuGetパッケージのパスを探索するよう実装することでこれを解決した。

ということで、FsBulletML.TypeProvidersリリースしました。

f:id:zecl:20140825181636p:plain


型プロバイダーが参照するファイルの更新チェックを実装する

型プロバイダーが想定するスキーマが変更された場合、F# 言語サービスがそのプロバイダーを無効化するようにシグナルを通知することができる。シグナルが通知されると、型プロバイダーがVisual Studio上でホストされている場合に限り、再度型チェックが行われる。 これを利用して型プロバイダーが参照するファイルの更新チェックを実装することができる。具体的には、FileSystemWatcherクラス等でファイルの状態を監視し、適切なタイミングで CompilerServices.ITypeProvider インターフェイス (F#)のInvalidateメソッドを呼び出すように実装すればよい。FsBulletML.TypeProvidersでも実装しています。


ソースはここにあります。 FsBulletML/src/FsBulletML.TypeProviders at master · zecl/FsBulletML · GitHub


消去型と生成型

最後に、@bleisさんのTypeProviderについて、勝手に補足で紹介されていた生成型の型プロバイダーのひじょーにシンプルな例についてご紹介。


まず、消去型のサンプル


型が消えてますね。

f:id:zecl:20140825181814p:plain


f:id:zecl:20140825190217p:plain


ILDASMで逆コンパイルした結果も見てみましょう。 はい。型が消去されています。


これを生成型の型プロバイダーに書き直してみます。

ポイントは、ProvidedTypeDefinitionIsErased = falseとすること。 ProvidedAssemblyで一時アセンブリを作り、そのアセンブリAddTypesで生成する型を登録することです。 この例では、アセンブリ内に定義したPiyoクラスを継承する型を生成しています。

f:id:zecl:20140825181847p:plain


f:id:zecl:20140825181909p:plain


ILDASMで逆コンパイルした結果を見てみましょう。


Visual Studio上でも確認ができたように、型が消去されずに、Piyoクラスを継承したHogeAクラスが型プロバイダーによって生成されていることが確認できます。




うほ!とても面白いアイデア。 生成型の型プロバイダーだと確かにそういった構想の面白いブツが作れそうですね(wktk)。

ということで、型プロバイダー(Type Provider)のちょっとしたアレコレを書いてみました。それはそうと、Visual F# Power Toolsなのか他の拡張機能なのかわからないけど、型プロバイダーを書いたりデバッグしていると、割と頻繁になにかしらの拡張機能のエラーのダイアログがでてきてウザいですよ(激おこ)。