読者です 読者をやめる 読者になる 読者になる

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プロジェクトを作成します。

f:id:any-programming:20170225215642p:plain

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でも取得することも可能です。

f:id:any-programming:20170225215800p:plain

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は必要ありません。