F# --- TypeProvider --- クイックスタート
F#にはコンパイル時に型を生成するTypeProviderという仕組みがあります。
(実際にはインテリセンスを働かせるため、コンパイル時以外に逐次生成されています。)
今回は独自のTypeProviderを作成していきます。
作成するTypeProvider
指定した次数のベクトル型を作成するTypeProviderを実装していきます。
type Vector2 = Vectors.Vector<2> let v = Vector2(1.0, 1.0) let x = v.X1 let y = v.X2
dllプロジェクト作成
TypeProviderはdllとして作成しますので、F#のLibraryプロジェクトを作成します。
ProvidedTypes.fs
ProvidedTypes.fsを利用することにより、一から実装する必要がなくなります。
GitHubにて"FSharp.TypeProviders.StarterPack"として公開されていますので、そこからsrcの中のProvidedTypes.fsをコピーしてきます。
GitHub - fsprojects/FSharp.TypeProviders.StarterPack: The ProvidedTypes SDK for creating F# type providers
またNuGetでも取得することも可能です。
TypeProviderの定義
クラスにはTypeProvider属性を付与し、dllにはTypeProviderAssembly属性を付与します。
[<TypeProvider>] type VectorTypeProvider() as this = inherit TypeProviderForNamespaces() // 実装 [<assembly:TypeProviderAssembly>] do ()
パラメータを受け取るTypeProviderの実装
intを受け取るVectors.Vectorを定義しています。
ProvidedStaticParameterを増やせば、渡せるパラメータも増やせます。
let asm = Assembly.GetExecutingAssembly() let ns = "Vectors" let vectorType = ProvidedTypeDefinition(asm, ns, "Vector", None) do vectorType.DefineStaticParameters( [ ProvidedStaticParameter("dimensions", typeof<int>) ], (fun typeName args -> // 実装 )) this.AddNamespace(ns, [ vectorType ])
型の作成
double[]と定義しているのは、Vectorの要素の値を保存する場所を確保するためです。
TypeProviderには消去型と生成型の2パターンあり、今回は消去型を利用しています。
消去型では実際に新しい型が生成されるわけではなく、指定した型(ここでのdouble[])とオペレータが作成されます。(外側からは、あたかも新しい型ができたように見えます。しかしリフレクションなどでは期待した挙動とは異なります。)
let t = ProvidedTypeDefinition(asm, ns, typeName, Some typeof<double[]>)
コンストラクタ作成
ProvidedParameterで引数を定義し、ProvidedConstructorのInvokeCodeにコンストラクタの挙動を定義します。
InvokeCodeには入力からExprを生成する関数を設定します。ここではコンストラクタのパラメータからdouble配列を作成しています。
let dimensions = args.[0] :?> int let ctorParameters = [ 1..dimensions ] |> List.map (fun x -> ProvidedParameter(sprintf "X%d" x, typeof<double>)) let ctor = ProvidedConstructor(ctorParameters, InvokeCode = (fun args -> Quotations.Expr.NewArray(typeof<double>, args))) t.AddMember(ctor)
プロパティ作成
double配列から値を取り出すプロパティを定義します。
挙動はGetterCodeに記述します。<@@ @@>で囲まれた区間はExprとして解釈されます。
%%をExprの先頭につけると値の様に扱うことができます。
ちなみに<@ @>や%はExpr<_>に用いることができます。
[ 1..dimensions ] |> List.map (fun x -> let property = ProvidedProperty(sprintf "X%d" x, typeof<double>) property.GetterCode <- (fun args -> <@@ (%%args.[0] : double[]).[x - 1] @@>) property) |> List.iter t.AddMember
コード全体
namespace VectorTypeProvider open Microsoft.FSharp.Core.CompilerServices open ProviderImplementation.ProvidedTypes open System.Reflection [<TypeProvider>] type VectorTypeProvider() as this = inherit TypeProviderForNamespaces() let asm = Assembly.GetExecutingAssembly() let ns = "Vectors" let vectorType = ProvidedTypeDefinition(asm, ns, "Vector", None) do vectorType.DefineStaticParameters( [ ProvidedStaticParameter("dimensions", typeof<int>) ], (fun typeName args -> let dimensions = args.[0] :?> int let t = ProvidedTypeDefinition(asm, ns, typeName, Some typeof<double[]>) let ctorParameters = [ 1..dimensions ] |> List.map (fun x -> ProvidedParameter(sprintf "X%d" x, typeof<double>)) let ctor = ProvidedConstructor(ctorParameters, InvokeCode = (fun args -> Quotations.Expr.NewArray(typeof<double>, args))) t.AddMember(ctor) [ 1..dimensions ] |> List.map (fun x -> let property = ProvidedProperty(sprintf "X%d" x, typeof<double>) property.GetterCode <- (fun args -> <@@ (%%args.[0] : double[]).[x - 1] @@>) property) |> List.iter t.AddMember t)) this.AddNamespace(ns, [ vectorType ]) [<assembly:TypeProviderAssembly>] do ()
利用側
このTypeProviderを利用するクラスはdllを参照に追加します。
しかし実行の際にはこのdllは必要ありません。