ようこそ。睡眠不足なプログラマのチラ裏です。

TypeProviders に関するちょっとした小ネタ集

F# Advent Calendar 2016 の 22日目の記事です。

TypeProviderについては以前、 型プロバイダー(TypeProvider)のちょっとしたアレコレというのを書きました。 書いたのはそーとー前ですが、今でも割と役に立つかもしれない以下の話題について扱っているので、気になるものがあればどうぞ。

- 型プロバイダーに渡すことができる静的引数んの種類
- 型プロバイダーの実行部分は部分的な制限がある
- 他のDLLに依存する型プロバイダーを作る
- 他のNuGetパッケージに依存した型プロバイダーを作ってNuGetで配布するときのやり方
- 型プロバイダーが参照するファイルの更新チェックを実装する
- 消去型と生成型



  さて。この記事は、TypeProviderに関する役立つものから役立たないものまで雑多な小ネタをいくつか適当に書いていきます。何かひとつでも引っかかるものがあって、持ち帰ってもらえれたら幸いかなと思います。

TypeProviderを作る際の下準備

古くは、F# Type Provider Templateとゆーありがたいテンプレートが用意されていました!が、もう過去のもの。今は何も考えずに、まず FSharp.TypeProviders.StarterPackPaketなりNuGetなり、お好きな方法でインストールしましょう。こちらが現在メジャーなテンプレとなります。最新バージョンは現時点では1.1.3.88です。これさえあればすぐにTypeProviderの作成に取り組むことができます!(ありがてぇ)まぁ、少しかゆいところに手が届いてない感じも否めないですが、StarterPack という名前から察するにあえてやり過ぎないように意識しているのかもしれません。

  なので、StarterPackだけでは満たされないF#er諸兄は、お好みでHelper関数などを別途作ります。

module Shared = 
  type private T = interface end
  let thisAssembly = typeof<T>.Assembly


TypeProviderでは、自身のAssemblyを取得する必要があるので適当に生やすとか。お作法的には、TypeProviderConfig.RuntimeAssemblyから取得する方がよいのかなあ?まあたぶんお好みで大丈夫かと。
 

let assembly = Assembly.LoadFrom( config.RuntimeAssembly)


TypeProviderを作るためのシンプルなヘルパーをいくつか準備しておくのもいいでしょう。

type internal ProvidedTypes = 
  static member DefineProvidedType(namespaceName, className, ?baseType, ?assembly, ?isErased) = 
    let assembly = defaultArg assembly Shared.ThisAssembly
    let t = ProvidedTypeDefinition(assembly, namespaceName, className, baseType)
    isErased |> Option.iter (fun v -> t.IsErased <- v)
    t

  static member ConvertToGenerated(ty : ProvidedTypeDefinition) = 
    if ty.IsErased then failwith "消去型ではこの操作はきたいされません"
    let asm = new ProvidedAssembly(IO.Path.GetTempFileName() + ".dll")
    asm.AddTypes([ty])

  static member CreateSimpleErasedType(ns) =
    ProvidedTypes.DefineProvidedType(ns, "Erased", baseType = typeof<obj>)

  static member CreateSimpleErasedType(ns, typeName) =
    ProvidedTypes.DefineProvidedType(ns, typeName, baseType = typeof<obj>)

  static member CreateSimpleGeneratedType(ns) =
    let ty = ProvidedTypes.DefineProvidedType(ns, "Generated", baseType = typeof<obj>, isErased = false)
    ProvidedTypes.ConvertToGenerated ty
    ty

  static member CreateSimpleGeneratedType(ns, typeName) =
    let ty = ProvidedTypes.DefineProvidedType(ns, typeName, baseType = typeof<obj>, isErased = false)
    ProvidedTypes.ConvertToGenerated ty
    ty


ちなみにこちらは某サンプルにあるやつとほぼ同じやーつで。面白みは特にないです。

TypeProviderのデバッグ方法

TypeProviderデバッグ方法については、いろいろな人が何度か取り上げている気がしますが。あらためて。プロジェクトファイルfsprojに以下のような定義を追加し(VisualStudio上から設定可)、デバッグ時に別のVisualSudioが立ち上がるように設定してあげます。

    <StartAction>Program</StartAction>
    <StartProgram>$(DevEnvDir)devenv.exe</StartProgram>
    <StartWorkingDirectory>$(SolutionDir)</StartWorkingDirectory>
    <StartArguments>DebugSample.sln</StartArguments>


TypeProviderと同一のソリューション内でデバッグ用に作成したTypeProviderを参照しているプロジェクトの.fsファイルを開くと、その時点で Visual Studio氏が空気を読まずにDLLを掴みっぱなしにしてしまい、ビルドができなくなってしまいます。ファイルを開かないように気を付ければソリューション自体を分けなくともTypeProviderデバッグ実行は可能ですが、デバッグ用プロジェクトのソリューションは分けておいた方が、なにかと捗るかなと思います。デバッグしながら作っていてもよくわからないTypeProviderの世界へようこそ(´・_・`)

enumの作り方

enumを生成する雑サンプルです。

module ProvidedEnums =
  let Namespace = "ProvidedEnums"

  [<TypeProvider>]
  type TypeProvider() as this =
    inherit TypeProviderForNamespaces()
    let ty = ProvidedTypes.CreateSimpleGeneratedType(Namespace)
    do 
      ty.SetBaseType typeof<Enum>
        
    let field = ProvidedLiteralField("Apple", ty, 1)
    do ty.AddMember field
        
    let field = ProvidedLiteralField("Pen", ty, 2)
    do ty.AddMember field
    do this.AddNamespace(Namespace, [ty])

    let field = ProvidedLiteralField("Pineapple", ty, 2)
    do ty.AddMember field

    do this.AddNamespace(Namespace, [ty])


これを使うときは、
 

type Pikotaro = ProvidedEnums.Generated


このように書くとenumを生成することができます。 このシンプルな例ではField名はベタ書きの固定値を用いて生成されますが、任意の静的パラメータを渡すなりして任意の文字列ないし特定の外部ファイルの内容をもとに不特定多数のenumを生成できるようなものを作ることもできます。工夫をすることにより、それなりに使い道のあるTypeProviderに仕上げることができるかもしれません。

ちなみに想像に難しくはないと思いますが、消去型のTypeProviderenumを作ろうとしても、読んで字のなんとやら型情報が消えてしまいます!ので、enumを生成することができません。enumを作りたい場合は生成型で生成する必要があります。消去型と生成型の違いここにアリ!って感じですね。消去型と生成型の違いをシンプル且つわかりやすく説明するのにはよいサンプルでした。はい。

カスタム属性の付け方。FlagsAttribute を付与しよう

FlagsAttributeを付与したenumを生成する方法から、カスタム属性を付与しつつ型を生成する方法についてみてみましょう。

たとえば以下のように、CustomAttributeDataインスタンスを作るヘルパーを用意しておくと便利です。やっぱりオブジェクト式は便利ですね。はい。
 

module Attributes =
  //カスタム属性追加するやーつのヘルパー
  type CustomAttributeDataExtentions =
    static member Create(ctorInfo, ?args, ?namedArgs) = 
      { new CustomAttributeData() with 
        member __.Constructor =  ctorInfo
        member __.ConstructorArguments = defaultArg args [||] :> IList<_>
        member __.NamedArguments = defaultArg namedArgs [||] :> IList<_> 
      }


FlagsAttributeのようなコンストラクタで引数を要求しないAttribute向けには、たとえばこんな関数を定義しておきます(雑)。
 

  let createAttributeData<'TAttribute>() =
    CustomAttributeDataExtentions.Create(typeof<'TAttribute>.GetConstructor([||]), [| |])


適当なパラメータからFlagsAttributeを付加したenumを作る雑サンプルです。
 

module ProvidedFlagsEnums =
  let Namespace = "ProvidedFlagsEnums"

  [<TypeProvider>]
  type TypeProvider() as this =
    inherit TypeProviderForNamespaces()

    let root = ProvidedTypes.CreateSimpleGeneratedType(Namespace)
    do 
      let p = ProvidedStaticParameter("names", typeof<string>)
   
      root.DefineStaticParameters
        (
          parameters = [p],
          instantiationFunction = 
            (
              fun typeName names ->
                let ty = ProvidedTypes.CreateSimpleGeneratedType(Namespace, typeName)
                ty.SetBaseType typeof<Enum>

                let attrData = Attributes.createAttributeData<FlagsAttribute>()
                ty.AddCustomAttribute(attrData)

                let names = 
                  names |> Array.head |> string
                  |> (fun x -> x.Split(','))
                  |> Array.mapi (fun i x -> x,i - 1)                
                for name,i in names do
                  let field = ProvidedLiteralField(name, typeof<int>, Math.Pow(2.,float i) |> int)
                  ty.AddMember field
                ty
            )
        )
    do this.AddNamespace(Namespace, [root])


使い方はたとえばこうです。
 

type 属性 = ProvidedFlagsEnums.Generated<"なし,火遁,風遁,水遁,雷遁,土遁,木遁,溶遁">


サンプルなので雑に作りましたが、まじめに作ればそれなりに便利なものが作れそうな気はします。このように、生成する型に対してカスタム属性を付加することもできます。TypeProviderによって生成された型であるということを属性によってマークして明示する用途に使うこともできますし。いろいろと応用の幅はありそうですね。

引数のある属性であればこんな感じのを複数パターン用意したり
 

  let createAttributeData2<'TAttribute> arg1 =
    CustomAttributeDataExtentions.Create(typeof<'TAttribute>.GetConstructor([|arg1.GetType()|]),
                                [| CustomAttributeTypedArgument(typeof<'TAttribute>, arg1) |])


安全性を無視できるのであれば、雑にこういうの作ったりするのもまぁアリかもですね。
 

  let createAttributeData3<'TAttribute> args =
    let types = args |> Array.map (fun arg -> arg.GetType())
    let values = args |> Array.map (fun arg -> CustomAttributeTypedArgument(typeof<'TAttribute>, arg))
    CustomAttributeDataExtentions.Create(typeof<'TAttribute>.GetConstructor(types), values)


型安全絶対死守するマンを突き通して、ガチに頑張るのであればTryGetConstructor的なやつを作ったりしてまじめにやる感じかなあ?そのあたりはお好みでどうぞ。

TypeProviderに渡す静的パラメータをハードコーディングしたくないパターンのやつ

TypeProviderあるあるすぎて、なるほどたし蟹!と思ったしだいです。

静的パラメータをコードに直書きしたくないときは、TypeProviderから静的パラメータを作れればええやん!のパターンのやつ。以下のような感じのをつくっとけばサクッと実現できますね。

module FileReader = 
  open System.IO

  [<TypeProvider>]
  type public FileReaderProvider(config : TypeProviderConfig) as this = 
    inherit TypeProviderForNamespaces()
 
    let nameSpace = this.GetType().Namespace
    let assembly = Assembly.LoadFrom( config.RuntimeAssembly)
    let providerType = ProvidedTypes.CreateSimpleErasedType(nameSpace, "FileReader") 
    do
        providerType.DefineStaticParameters(
          parameters = [ ProvidedStaticParameter("Path", typeof<string>) ],             
          instantiationFunction = fun typeName [| :? string as path |] ->
            let t = ProvidedTypes.CreateSimpleErasedType(nameSpace, typeName) 
            let fullPath = if Path.IsPathRooted(path) then path else Path.Combine(config.ResolutionFolder, path)
            let content = File.ReadAllText(fullPath)
            t.AddMember <| ProvidedLiteralField("Content", typeof<string>, content)
            t
        )
 
        this.AddNamespace( nameSpace, [ providerType ])


雑サンプルではありますが、こーゆーの用意しておくと便利かもーですね。

#nowarn "25" してもええんじゃよ

お気づきの方もいるかもしれませんが、上記のFileReaderProviderでは、パターンマッチが不完全な記述があるため、警告がでてしまっています(!)

該当のコードを切り出すと、ちょうどここです。配列に対するパターンマッチが不完全ですね。あらまあ。
 

  instantiationFunction = fun typeName [| :? string as path |] ->


  メソッドやプロパティの実行コードを実装するためのInvokeCodeの型は Quotations.Expr list -> Quotations.Exprたったりもしますし、TypeProviderを作っていると、ArrayListの要素に対して直にアクセスするシーンが頻出します。ラムダ式の引数から静的パラメータを取り出す場合、直にインデックス指定でアクセスして値を取り出すのが割と正攻法な気がしていますが、まあある程度は割り切ってしまって、雑パターンマッチで済ませてパターンマッチの記述をさぼるのもアリかなあと思います。#nowarn "25" で不完全なパターンマッチの警告を無視してしまうのも方法としてはまあアリかなと。そう割り切れるか割り切れないかはまあ。好みの問題。お好きなように。

ただ、F#では 1度#nowarnしちゃうと、その行以下のファイル全体に対して影響が及んでしまう(イケていない)ので。ファイル内で再び警告を有効化できるように対応してもらいたいですね(はよ!)。

↓このあたりの話ですね。
Allow F# compiler directives like #nowarn to span less than an entire file.

#nowarn しないなら こんな感じのアクティブパターンでがんばります?になるのかな。だるそう(´・_・`)
 

  let (|Empty|NonEmpty|) l =
    match l with [] -> Empty | x::xs -> NonEmpty(x, xs, l)


ちなみに、F#er 諸兄の中で不完全なパターンマッチを絶対コロスマンの人ってどのくらいいるのでしょう。つまり、以下のような感じにコンパイルオプションでこの警告をコンパイルエラーに設定してる人のことです。  

--warnaserror:25

  まあこの手の警告がでたら、ほとんどの場合はすぐに直すのでコンパイラオプションでガチガチに指定するほどでもない説はかなりあります(´・_・`)

TypeProviderで生成される型のインスタンスへのアクセス

    let instance = 
      (%%(Quotations.Expr.Coerce ( args.[0], typeof<HogeBase>)) : HogeBase) 


Quotations.Expr listで最初に渡ってくるヤツがソレなので。そいつを Quotations.Expr.Coerceを用いて強制的に適切な型にキャストしてあげればよいです。 この方法で生成しようとしている型のインスタンスに対してコネコネ操作することができます。やったね。

有効な識別子かどうかをチェックしたりしてもよいかもね

静的なパラメーターによって動的に変化するメタデータを使用してC#ライクな型を生成するケースなどでは、例えば Microsoft.CSharp.CSharpCodeProviderを使用して、IsValidIdentifier(有効な識別子かどうか)など厳密にチェックするような実装を検討してもいいかもしれません。

module GeneratedTypesWithStaticParams =
  let Namespace = "GeneratedTypesWithStaticParams"
  let csCodeProvider = new Microsoft.CSharp.CSharpCodeProvider()

  [<TypeProvider>]
  type TypeProvider() as this =
    inherit TypeProviderForNamespaces()

    let top = ProvidedTypes.CreateSimpleGeneratedType(Namespace)
    do 
      let p = ProvidedStaticParameter("name", typeof<string>)
      top.DefineStaticParameters
        (parameters = [p],
         instantiationFunction = 
          (fun typeName [|:? string as name|] ->
            let ty = ProvidedTypes.DefineProvidedType(Namespace, typeName, typeof<obj>, isErased = false)
            ProvidedTypes.ConvertToGenerated ty
            if not (csCodeProvider.IsValidIdentifier name) then failwithf "'%s' is not an identifier" name
            ty.DefineMethod("Method_" + name, [], typeof<string>, isStatic = true, invokeCode = fun [] -> <@@ name @@>)
            |> ignore
            ty))
    do this.AddNamespace(Namespace, [top])


あるいは状況に応じて、RoslynFSharp.Compiler.Serviceを使ってほげもげー!ってガチに頑張るパターンもありけり(´・_・`)

static parameterをすべて省略したときのやり方のやーつ

静的パラメータありのTypeProviderで、すべての静的パラメータを省略できることをサポートするためには、渡されるすべてのProvidedStaticParameterについて、省略された場合の既定値を設定しておいてあげればよいです。

  [<TypeProvider>]
  type TypeProvider() as this =
    inherit TypeProviderForNamespaces()

    let top = ProvidedTypes.CreateSimpleGeneratedType(Namespace)
    do 
      let p = ProvidedStaticParameter("name", typeof<string>, "省略されたよ") //既定値を指定するよ
      top.DefineStaticParameters
        (parameters = [p],
         instantiationFunction = 
          (fun typeName [|:? string as name|] ->
            let ty = ProvidedTypes.DefineProvidedType(Namespace, typeName, typeof<obj>, isErased = false)
            ProvidedTypes.ConvertToGenerated ty
            if not (csCodeProvider.IsValidIdentifier name) then failwithf "'%s' is not an identifier" name
            ty.DefineMethod("Method_" + name, [], typeof<string>, isStatic = true, invokeCode = fun [] -> <@@ name @@>)
            |> ignore
            ty))
    do this.AddNamespace(Namespace, [top])


 

type GTWSP = GeneratedTypesWithStaticParams2.Generated<"ABC">
type GTWSP2 = GeneratedTypesWithStaticParams2.Generated //すべての静的パラメータを省略できている

let a = GTWSP.Method_ABC()
let b = GTWSP2.Method_省略されたよ()


という感じで、静的パラメータをすべて省略可能なTypeProviderを作ることもできますのでご活用ください。あまりお役立ち情報ではないうえ、知ってた速報!かもしれませんが。個人的にはなかなか気づけなかったんですよねコレ(´・_・`)

base..ctorを呼び出す方法

生成する型のbaseType の base..ctor をコンストラクタで呼び出すには、ProvidedConstructorBaseConstructorCallでよしなに処理してあげればよいです。

module Constructors1 = 
  let Namespace = "Constructors1"

  [<TypeProvider>]
  type TypeProvider() as this =
    inherit TypeProviderForNamespaces()
    let resizeArrayTy = typeof<ResizeArray<int>>
    let ty = ProvidedTypes.DefineProvidedType(Namespace, "Generated", baseType = resizeArrayTy, isErased = false)
    do
      ProvidedTypes.ConvertToGenerated ty
      let ctor = ProvidedConstructor([ProvidedParameter("x", typeof<string>)])
      ctor.BaseConstructorCall <- //ここんとろね
        fun [this; _] ->
          let ci = resizeArrayTy.GetConstructor([| typeof<int>|])
          ci, [ this; <@@ 100 @@>]
      ctor.InvokeCode <-
        fun [this; arg] ->
          let addMethod = ty.GetMethod "Add"
          let add = E.Call(E.Coerce(this, resizeArrayTy), addMethod, [ <@@ int (%%arg : string) @@>])
          <@@
              (%%add : unit)
              (%%E.Coerce(this, resizeArrayTy) : ResizeArray<int>).Add(int (%%arg : string) + 100)
          @@>
      ty.AddMember ctor
      this.AddNamespace(Namespace, [ty])

メソッドのオーバーライドで、baseのメソッドを呼び出すときの書き方

let baseMathod = methodInfo.GetBaseDefinition() :?> ProvidedMethod

で取得した baseMathodInvokeCode内の任意のタイミングで呼び出せばたぶんできます(雑すぎ)

abstract メソッド/プロパティをオーバーライドするときの書き方

TODO:あとで書くかも(そーゆー場合だいたい書かない)

任意のインターフェイスを実装するときの書き方

TODO:あとで書くかも(そーゆー場合だいたい書かない)

staticコンストラクタとstatic fieldの書き方

staticコンストラクタとstatic fieldをつくるサンプル

module StaticConstructorAndStaticFields =
  let Namespace = "StaticConstructorAndStaticFields"

  [<TypeProvider>]
  type TypeProvider() as this = 
    inherit TypeProviderForNamespaces()
    let top = ProvidedTypes.DefineProvidedType(Namespace, "Generated", baseType = typeof<System.Windows.Controls.Button>, isErased = false)
    do ProvidedTypes.ConvertToGenerated top
    let propertyName = "StateProperty"
    let f = ProvidedField(propertyName, typeof<System.Windows.DependencyProperty>)
    do 
      f.SetFieldAttributes (FieldAttributes.Public ||| FieldAttributes.InitOnly ||| FieldAttributes.Static)
      top.AddMember f
    do
      let typeInit = ProvidedConstructor([], IsTypeInitializer = true)
      typeInit.InvokeCode <- 
        fun [] -> 
          Quotations.Expr.FieldSet
            (
              f, 
              <@@ 
                System.Windows.DependencyProperty.Register(propertyName, typeof<bool>, top, Windows.PropertyMetadata(false)) 
              @@>
            )
      top.AddMember typeInit
    do this.AddNamespace(Namespace, [top])

とゆー感じで書けるます。 この例とは直接関係はありませんが、生成対象のclassに get only なプロパティーを生やす場合の実装方法として、返却する値はリフレクションを使ってプロパティに突っこんでおけばええやん!説が、私の中では割と正義だったのですが。Quotations.Expr.FieldSetを用いて、privatefieldに値を設定しておて、それをget onlyなプロパティで返してあげるように実装してあげる方がキレイなのかな。と思い直している今日この頃です。でもまあ、力強くリフレクションで済ませてしまう方が実装楽ではあります(雑にそう書いてもバチはあたらないでしょう!)。

Quotations.Exprを活用しよう

そんなこんなで、Quotations.Exprに生えている各種 static member を把握してうまく活用することで、TypeProviderでできることの幅がぐんと広がるかと思います。

↓を参照してください。
Quotations.Expr Class (F#)

少しだけ例を紹介しようかなとも思いましたが、長くなるので雑にリンクだけ貼っておきます(読めば察せると思いますので!)。

TypeProvider の静的パラメータに任意の型を渡せるようになる時代がくーるー!?

F# RFC FS-1023 - Allow type providers to generate types from types ってゆー話があり。次期バージョンの F# でこれがサポートされると...、もうやりたい放題!!ですね。夢が広がりんぐ。はやくきてくれ~(´・_・`)

ひとつの TypeProvider から異なる複数の型(生成型)を生成できるようにする方法

消去型のTypeProviderにおいてはまったく意識する必要はないのですが、生成型のTypeProviderでは意識すべき内容です。

type Fuga = HogeTypeProvider<"Fuga">
type Piyo = HogeTypeProvider<"Piyo">

上記のように1つのTypeProviderを用いて複数の異なる型を生成する。こんな当たり前(そう)なことが、生成型のTypePrpviderにおいては、そうできるように実装を考慮をしていないと実現できません。考慮せずに実装した場合は、たとえばおおよそ以下のような感じのエラーが出てしまい、ほげーーーっと剥げてしまいます。

    重大度レベル   コード   説明  プロジェクト  ファイル    行 抑制状態
    エラー       バイナリ 'D:\Code\F#\TypeProviders\TypeProviderSample\ConsoleApplication1\obj\Debug\ConsoleApplication1.exe' の書き込み中に問題が発生しました: Error in pass3 for type Program, error: Error in GetMethodRefAsMethodDefIdx for mref = (".ctor", "Hoge"), error: 種類 'Microsoft.FSharp.Compiler.AbstractIL.ILBinaryWriter+MethodDefNotFound' の例外がスローされました。  ConsoleApplication1 FSC 1

このようなエラーを回避するには、TypeProviderの各ルートタイプに対して個別の一時アセンブリを作成してあげる必要があります。この考慮が割と忘れがちになります(TypeProvider実装考慮漏れあるある)。生成型のTypeProviderを作るときのお決まりのパターンというか、ある種嗜みのようなものかなと思いますので、覚えておいて損はないかと思います。

やり方については以下を参照してもらえればよいかと思います。

Creating more than one root type with generative type provider - stackoverflow
https://gist.github.com/dsevastianov/46d1a8495c4af46a9875

おれたちのVisualSudioさんが、期待ている config ファイルを参照してくれない件について

TypeProviderが参照するDLLからConfigurationManagerを使用してapp.configから情報を取得しようとした場合、われわれが期待するapp.configを参照してくれないことがある!とゆ-残念無念な問題があります。VisualStudioでホストしてTypeProviderを動作させた場合、devenv.exeからみた configファイルを対象として動作してしまうのが原因です(どーしよーもない)。

devenv.exe(VisualSudio)からみたconfigは、われわれが期待するソレではなく、以下のようないわゆるVisualStudioの在り処にあるconfigファイルを対象に動作します(これはひどい罠)。

C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\IDE\devenv.exe.config

TypeProviderから参照されるDLLからConfigurationManagerを用いて、適切にわれわれが期待するapp.configを読み込みたいケースでは、なんらかの方法で対象のPathをConfigurationManagerに直接指定せざるを得ないです。まあ、この問題にぶちあたるケースの方がよっぽどレアケース説!がありすぎて。ほぼ誰の役にも立たない情報かもしれません。

TypeProviderをテストするためには

FSharp.TypeProviders.StarterPackの中の人でもある Mavnn(@mavnn)さんがまとめてくださっているので、くわしくはそちらをご参照ください!(THE丸投げ)。
Testing ProvidedType.fs by Example

Cross-Targeting Type Providers

素でFSharp.TypeProviders.StarterPackを使っているとなかなか気付きにくいことですが、FSharp.TypeProviders.StarterPackのGitHubリポジトリの方を見に行くと、Cross-Targeting Type Providersとかなんとか何やら興味深い記述が。

リポジトリにある以下の3つのファイルを追加すれば利用できるとのこと。

- AssemblyReader.fs
- AssemblyReaderReflection.fs
- ProvidedTypesContext.fs

これ、実はPaketNuGetを介して最新版を取得しても、まったく降ってこないんですよね。気づきにくい(!)  

軽い気持ちでコードを読もうとしても黒魔術すぎてわたしのような雑魚にはぶっちゃけわけわかめ(難しい)なんですが、AssemblyReader.fsは軽量.NETアセンブリリーダー、AssemblyReaderReflection.fsはディスク上のアセンブリに対するリフレクションオブジェクトの実装。ProvidedTypesContext.fsは、mscorlibまたはSystem.RuntimeのDLLが見つからないような環境下において、軽量.NETアセンブリリーダーを使用してよしなにCross-Targeting対応してくれるやーつみたいな感じなのかな。たぶん。

これの使い方自体は簡単で。ProvidedTypesContextでコンテキストをつくって

let ctxt = ProvidedTypesContext.Create(config)

  そのコンテキスト経由で各種Providedほげもげを生成してTypeProviderを構成していけばよいみたいです。ほーぅ。

let myType = ctxt.ProvidedTypeDefinition(asm, ns, "MyType", typeof<obj>)

実装構成によっては割と面倒くさいことになり兼ねない感じがプンプンします(が、そんなときはReaderモナドとかでうまいこと乗り切ってください(察し))

以下あたりも読んでおきましょうかね。
Developing Cross-Platform and Multi-Targeting Type Providers

ということで、とりとめもなくTypeProviderに関する小ネタをいくつか書いてみました。まだまだネタは尽きないような気もしましたが、私の気力がちょうど尽き(and 時間切れ)たところで終わります。あとで書くかものやつは、書かない可能性が高そうですが大目に見てください(誰かが書いてくれてもいい)。  

ところで、TypeProviderは、弊社次期タイトル「黒騎士と白の魔王」でもガチで使用されています。リリースされましたら、そのあたりにも注目してみてください(まったく表に出てきませんので注目のしようがありませんが!)。どのように使われているのかについてお話する機会が持てるかどうかはわかりませんが。その時は面白い話もできるかもしれません。つらつらダラダラと書きましたが、何かひとつでも参考になれば幸いです。

TypeProvider作っていきまっしょい(圧倒的情報不足のため、どんな情報でも共有お待ちしています)

Comm Tech Festival D-3 「open FSharp」 (続きはWebで)

@ufcpp さんにお話しをいただいて Comm Tech Festival に参加してきました。ありがとうございました。 comuplus.doorkeeper.jp

「open FSharp」というタイトルで発表させていただいたのですが、時間配分がうまくいかずにスライドの最後まで紹介することができませんでした。申し訳ありません(はずかしい)。

「続きはWebで。」とお約束をしていたので、こちらにまとめます。使用したスライドをそのまま公開することも考えましたが、それだと内容が伝わりにくくてあまり価値がなさそうかなと思ったのと、変な誤解をされる恐れもあると考えたので、発表時に話した内容に簡単な補足を加えたり、ときにはざっくり省いたりしたかたちでこちらにまとめることにしました。


時間オーバーしただけあってだいぶ長いです(ポエム)。
セッションを聞いていただいたかたは、続きのみどうぞ

f:id:zecl:20150927154047j:plain
f:id:zecl:20150927154123j:plain

@mayukiさんが作ってくれた画像。あまり深くは触れませんが、IQ145 がだいぶやばいです。別に殺伐とはしていなかったはず。

f:id:zecl:20150927154337j:plain
基本的には、F#が"ふつうに関数型言語である"というありがたみのない話

f:id:zecl:20150927154435j:plain
f:id:zecl:20150927154509j:plain
言語遍歴としてはVB(レガシー的なのも含む)、C#Java、あとはDelphiあたり。その他いろいろさわってますが、よく知られている(一般的な)オブジェクト指向言語をつかって仕事をしていることが多かったです。
かつて関数型言語関連の研究をしていたとか、数学が得意だとかでもなく一般の人。

C#の会社に所属していながら、F#の話をする。どうなんだ?という感じですが、理解ある会社、同僚たちと働かせてもらえてありがたいことです。


f:id:zecl:20150927154644j:plain
弊社開発者、結構 F# インストールしてくれています。使ってくれている(or 今後使ってくれる)かどうかはわかりませんが。 こちらのツイートをみたとき、まゆきさんツンデレだなあと思いました。

f:id:zecl:20150927154943j:plain
自宅でのVSのデフォルト環境は F# にしてます(だから何)

f:id:zecl:20150927155131j:plain
・FsBulletML
むかし一世を風靡した(?)弾幕記述言語BulletML(ABA Games)のF#実装。
特徴としては、判別共用体による型付き内部DSLXML、SXML、FSB(独自形式)の3種類の外部DSLを備えていることです。
また、各外部DSLから内部DSLへのコンパイル時変換を可能とする FsBulletML.TypeProviders も提供しています。NuGetである程度DLされているんだけど、使ってみたという話を一度も聞いたことないですね。

・UniFSharp
Unityエディタ上からMSBuildを実行してF#をビルドするやつ。
疑似的にF# ScriptをインスペクターにD&Dできたりします。あと、無駄にUnityちゃんがしゃべります。Unity で F# がサポートされるようになる日は、まずこなさそうですね。

f:id:zecl:20150927155515j:plain
F#に対して「無関心」だった人に関心をもってもらいたい。F#に対してもともと「関心」があった人をより協力者に近づけたい。というのを目標(目的に)

>どうして F# を流行らせたい?
単純に良いと思うものは広めたいし。語り合える仲間が増えるのはうれしいよね!というのは当然として、ユーザーが少ないことは問題でしかないと考えるからですね。実際ユーザが少なくてイイことなどまるでないので。開発者が少ないということは、ライブラリやフレームワークの選択肢の幅も狭くなります。書籍も出版されにくいし、Web上でも情報が少なく何かトラブルがあったときに困る可能性があります。んなもんで、ビジネスにも適用されにくくなります。とかいう、ごくふつうの話

f:id:zecl:20150927160039j:plain
実はVisual Studio 2010からいました

f:id:zecl:20150927160142j:plain
MSRとして、ML(Meta Language)的なアプローチを使った言語設計で研究することが決定したのが2002年らしいので、F#1.0に至るまで約3年かかったということになるのかな。
F#の父 ドンちゃんこと Don Syme 氏は、1998年からマイクロソフトで働き始めて.NET CLRC#(VB) のためのジェネリック機構を作った人。すごい人 #実際すごい

F#の特徴的な機能としては、sequence expression(シーケンス式)、非同期ワークフロー、computation expressions(コンピュテーション式)、Type Provider(型プロバイダー)などの新しい(面白い)試みがある。小さなところでいうと、Active Pattern,(アクティブパターン)なんてのもある。簡単に言うとユーザ指定形式のパターンマッチ。

最新版のF#4.0は、3.0からの大きな機能拡張はなくて割と地味な印象がある。でも、細かなバグの修正だったり手触り感の向上があって普通に便利になりました。あと、デフォではVisual Studioでインストールされなくなりましたが。このあたりは別にどっちでもいいです、

f:id:zecl:20150927160739j:plain
他には、毛色が違うけどリアクティブプログラミングもパラダイムのひとつと言って差支えないかな。

F#は関数プログラミングに主軸を置いたマルチパラダイム言語です。もっと言うと、F# は .NET で 実用レベルで使用できる関数プログラミングパラダイムをサポートしている唯一の言語。それ以上でもそれ以下でもないです。

C#(VB)はオブジェクト指向プログラミングを主体としたマルチパラダイム言語。とはいっても、C#(VB)のそれは関数プログラミングを支援している関数型プログラミング言語に比べて、ごくごく限られていて。C#(VB)でできるプログラミングのパラダイムの1つに関数プログラミングを加えるは、ちょっと違うかなと(個人的に)は思うところです。もちろん、LINQ使用時のラムダ式の利用など関数プログラミングのエッセンスが効果的にとりいれていて、それはオブジェクト指向プログラミングとも非常になじんでいます。

f:id:zecl:20150927161826j:plain
Monoがあることによって、いろんなところで動くよ
他には、GPUとかブラウザ上とか。さまざまな環境で動くF#さん

f:id:zecl:20150927161940j:plain

Fsharpbindingプロジェクトってのがあってですね・・・いろんなエディタいろんな環境でつかえるよ。 https://github.com/fsharp/fsharpbinding

でも、いちばんのオススメは、やはりVisual Studio(現時点ではおそらく最強)。Visual F# Power Tools 拡張機能の充実ぶりがすばらしいので。
一応条件付きとはいえ、Community Editionでフルに使えますし #いい世の中になりました

C#で作られたライブラリも利用可能、とは言ったものの、F#から使いやすいC#ライブラリとして設計されていないと、だいぶツライ感は否めないですけど。はい。

f:id:zecl:20150927162108j:plain
Visual F# Power Toolsの機能をいくつか紹介しました。

f:id:zecl:20150927162153j:plain
C#VBと同等にXMLドキュメントを生成してくれるやつ

f:id:zecl:20150927162408j:plain
クイックサーチ機能ですね

f:id:zecl:20150927162415j:plain
VS標準ではfsproj内のフォルダ作成はサポートされていないので、 フォルダをきって、階層構造をつくれるようになるのはふつうにうれしい。

f:id:zecl:20150927162426j:plain
これまではインターフェイスの実装は、すべて手で記述する必要があった…(圧倒的作業感) 自動生成はありがたい(実際助かる)

f:id:zecl:20150927162635j:plain
レコード型のスタブを生成する機能

f:id:zecl:20150927162706j:plain
判別共用体のパターンマッチケースを網羅的に自動生成してくれるやつ(こちらも助かる)

f:id:zecl:20150927162749j:plain
必要な名前空間やモジュールを追加してくれるやつ C#でいうところの、必要な using ほげもげ を追加してくれるやつ

f:id:zecl:20150927162844j:plain
発音はプラジナでよいのかな



f:id:zecl:20150927163121j:plain
githubですでにα版が公開されたので、興味のある人は見てみたらよいんじゃないでしょうか #か msrccs.github.io www.nuget.org

f:id:zecl:20150927163317j:plain
プログラミングスタイルと、それの支援状況的なアレ

f:id:zecl:20150927163421j:plain
関数型言語関数プログラミングなどは定義が結構あいまいで説明が難しい

f:id:zecl:20150927163612j:plain
おおむね間違っていないと思うが、個人的な見解と雑さがやばい

副作用 is 何 という話もあるが、深入りはやばい #実際やばい ので、ここでは破壊的代入操作や、コンソールや画面描画などの出力を伴う操作ということでお茶を濁す

f:id:zecl:20150927163710j:plain
高階関数の扱いやすさが、 関数型言語っぽさに比例しているかなあと、個人的には考えます。まぁ、関数型言語かどうかを端的にあらわすのであれば、ラムダ計算に基づいているかとかで判断するほうが妥当なのかなあ? 最近のプログラミング言語は、マルチパラダイム化(というか、関数プログラミングの一部の要素が取り入れられる)が進んでいてその境界が割と曖昧になってきている。が、関数プログラミングというスタイルについて 言語(コンパイラ)や開発環境でどの程度支援されているか(推奨されているか)どうかで、関数型言語と呼んで差支えがないかどうか判断をする感じかなあ。

f:id:zecl:20150927163841j:plain
これ。関数型妹botかなんかが流行っていた時期につぶやいたものだったかな? オブジェクト指向プログラミング言語は、オブジェクト指向プログラミングがなんたるかを教えてくれない。 では、関数型言語はどうだろう? かなりのぶぶんで言語そのものから関数プログラミングのスタイルを学ぶことができる。 ある面では学習コストは高いけど、そういった面で考えると、頭の中がまっさらならむしろ学習コスト低いのかななんて。

理解するのに時間がかかるような難しい理論や概念ももちろんありますが、大部分が誤解かと思います。関数型言語を使って関数プログラミングを学ぶ、その入り口自体には難しい要素はほとんどなく、シンプルかつ堅牢なルールが重んじられる(ことが多い)ので複雑性が少なくなって、むしろ簡単とさえ感じる部分もあるくらいです。少なくとも、可変な値に翻弄される複雑性を減らすことができるので、書きたいプログラムそのものに集中できる時間が増える。ということは実際に利用してみると実感できるはずです。

ただし言語によっては大きくシンタックスが異なる場合があるので、ある程度の抵抗感が生まれてしまうのは事実(しょーがない)。F#も「見た目がきもくて無理」なんて言われることもある。好んで使っているひとは「美しい」って言う人多いのに。

F# は(いい意味で)いろいろとゆるいので。みゅーたぶるな値も使い放題だし、手続き的だったりオブジェクト指向なスタイルを残しつつ、少しずつ関数プログラミングのスタイルを取り入れていくことが無理なくできるのではないかな。関数型言語のなかでも、とりわけ難しくない方ではないかなあと思う(個人の感想)

f:id:zecl:20150927164342j:plain
だいぶ釣りだし、実際のところ1分じゃぜんぜん足りない(そりゃそうだ)

某F# for fun and profit という素敵サイトの記事から引用したものです
fsharpforfunandprofit.com
重要なところも割とさっくりと大胆に省略されていたりしますが、まあイサギヨサがある。

そもそも、F# のコードを見たことがない人も多いのでは?ということで入れました。
かなりさらっとやるつもりでいたんですが、ここにかなりの時間を費やしてしまったので時間切れになってしまった。

f:id:zecl:20150927170525j:plain
let キーワードで 不変な値に名前をつけて定義します
明示的に値の型を記述していないこと注意してください(型はコンパイラが推論してくれます)

f:id:zecl:20150927170554j:plain
F# のリストは、順序が指定されていて変更不可の一連の同じ型の要素を表します。

リストを定義するには、セミコロンで要素を区切って角かっこで囲む感じ。
単一要素とリストの結合はコロン2つで。リスト同士の結合は@で。

f:id:zecl:20150927170705j:plain
F# では 返り値を返すときに return キーワードを使用しません。常に関数内の最後の式が返される。
最初こそ少しとっつきにくいかもしれないが、シンプルなルールなのですぐに慣れることができるだろうし、実際明確でわかりやすい。

f:id:zecl:20150927170754j:plain
ふぃぼなっち

f:id:zecl:20150927170839j:plain
リストから偶数のみを取り出す関数
偶数かどうかを判断する isEven が関数内関数として定義されている

リストの対する操作は Listモジュールに定義されているので、よしなに使う。
List.filterはLINQでいうところのWhereのこと。

f:id:zecl:20150927170944j:plain
1から100までのリストについて、正方形の面積を求めてその和をだす(なんのために)

関数に適用される引数を明確にするためには括弧を用います。
この例に関していうと、括弧を書かないとコンパイルが通らないので。

f:id:zecl:20150927171103j:plain
正確には前方パイプライン演算子

F#ではコードを書きやすく、また見通し良くするためのパイプライン演算子が標準で提供されている。

f:id:zecl:20150927171138j:plain
無名関数(ラムダ式)を書くときは fun キーワード使う。 もしかすると楽しい気分を演出するかもしれないし、しないかもしれない

f:id:zecl:20150927171333j:plain
完全に一致(ではない)が、似ている。

F#ではループの代わりとして利用できる高階関数が数多く提供されている。ので、ループしようと思ったら再帰関数書かなきゃだめなの!?みたいな変な誤解は持たないでほしい。C#(VB)のLINQになじみがあれば、LINQ to Objectsを使えば for や foreach などのループ用の構文を必要としない場面が多いということはわかってもらえるはず。

f:id:zecl:20150927171612j:plain
LINQの場合は当然、あらかじめ定義されている拡張メソッドなりがある場合のみにメソッドチェーンとして処理を手続き的に書き下すことができる。それに対してパイプライン演算子によるチェーンでは、型さえ合えば自由に関数をチェーンできる。果てしなく。
ただ、後ろになんでも繋げられることをメリットととらえるか、はたまたデメリットととらえるか。どちらも正しい。これは、開発のスタイルや重視するプログラミングパラダイムによって意見が変わってくるところだろうと思う。関数プログラミングを主体とするF# においては、パイプライン演算子によって後続の処理を好きなだけ連ねることができるのはメリットでしかない。

f:id:zecl:20150927171906j:plain
3年前のツイートだった

わたしはこれでF#覚えました。これは別に冗談で言っているのではなく結構本気で。(|>)パイプライン演算子という中置演算子を用いることで、関数と引数の順序を逆にすることができる。これによって、関数適用の流れを手続き型的に書き下すことができるので、処理の流れがわかりやすくなります。F#ではこの「パイプライン演算子を使う」ことが基本でありながら、同時に最上級のプラクティスであると言っても過言ではなく。これを好んでよく使うことはおのずとF#の上達に繋がります。

f:id:zecl:20150927172312j:plain
複数の値を組みにするやつ。
雑に言うと、フィールド名を持たない無名クラスのようなもの

f:id:zecl:20150927172348j:plain
System.Tuple.Create<,>とか書かなくてよいし、Item1,Item2…と付き合わなくてもよいという話。
多くの C# 開発者は タプル扱いやすく改善されないかなぁと思っているはず。 ちなみにF#のそれ。この場合かっこ(パーレン)も省略できる
あ、匿名型は F# にはないです。 F# でもたまーに欲しいシーンはあるので、ちょっと欲しいです。

f:id:zecl:20150927172657j:plain
match式は雑にいうと、 switch 文の強力版

ただし、match式はパターンマッチのほんの1例にすぎない。この例だと、あまりありがたみはないが、F#にはさまざまな形式のパターンマッチが提供されているので、非常に便利。

f:id:zecl:20150927172823j:plain
雑に言うと、タプルの各要素(フィールド)に名前つけたやつ 名無しの権兵衛さん

f:id:zecl:20150927172852j:plain
レコード型をパターンマッチで分解しているの図。 突然の and !だったりするが華麗にスルーで。

f:id:zecl:20150927172959j:plain
リストもパターンマッチで分解できる例

他にも、ユーザ指定形式のパターンマッチであるところの、Active Pattern,(アクティブパターン)なんてのもある。

f:id:zecl:20150927173044j:plain
なんか見たことあるやつ。動物抽象クラスを継承して動物たちが鳴くやつ。

f:id:zecl:20150927173206j:plain
突然の function式 ! だが、ここもスルーで(機能的にはmatch式と同じでその引数省略できる版)

いわゆる代数的なデータ型的なやつ。一部誤解を与えてしまう可能性もあるが、ある種のクラス階層を表現するのに使える。
オブジェクト指向プログラミングの「継承」と明らかに違う点は、まったく性質の異なるものを同質のものとみなして定義することが出来る点、かなあと思う。

f:id:zecl:20150927173514j:plain

これを難しいと言われると、もうどうしようもない。

f:id:zecl:20150927173835j:plain
カーリーブレイス。まあつまり中括弧のことなわけですが。洞窟物語のキャラにカーリーブレイスって居たなと。

スコープがインデントで区切られるという構造のオフサイドルール(軽量構文)。Pythonインスパイア。
これに対して、インデントにしばられない冗語構文もあるが、(どうとでもなるので)そっちは別段覚えなくてもよいと思う(個人の感想)

オフサイドルールは最初こそ少しとっつきにくいかもしれないが、ルールがシンプルなのですぐに慣れることができるだろうし、実際明確でわかりやすいように思う。ただ、人によっては「見た目キモすぎぃぃぃ」と言う反応をする方もいる。それも理解できないこともない。慣れないものを見ると拒絶反応を示すというのはごくごく当たり前の反応だし。無理なものは無理だし。生理的に受け付けないので付き合えません!(No Thank you!)とかもまあ現実世界でも実際あるわけだし、世知辛いですね(なんの話)

f:id:zecl:20150927174252j:plain
唐突には挿入した息抜き用の画像。 ぐらばく(@Grabacr07) さんが作ってくれたものです。頼んでもいないのに。(写真の人物本人の使用許可あり) かなりの謎コラだけど、非常によい表情です。

f:id:zecl:20150927212744j:plain

f:id:zecl:20150927174834j:plain
変数は、値にわかりやすい名前を付けて定義しておくことで、続くコードで再利用するためのもの。
let で変数を作り出すことを変数束縛と言います(くどい)

f:id:zecl:20150927175027j:plain
例えば破壊的代入(再代入)を許すようなプログラムでは、変数が異なる状態を持ち得るのでその変数を参照する場合「この変数の今の値は何だろう?」と観測しなければならなくなり、複雑性を生み出す要因となる。つまり心配ごとが増える。

f:id:zecl:20150927175436j:plain
let による変数束縛ではデフォルトで破壊的代入(再代入)ができません。が、mutable宣言をすることで、破壊的代入が可能な変数として宣言することもできます。しかし、関数プログラミングというスタイルでは極力「破壊的な状態の変化」というものを避ける傾向にあるので、破壊的代入をデフォルトで許さないようにしている。これはスタイルとしての「副作用をできるだけ使わない」ということを推奨/支援しているとも言える。

f:id:zecl:20150927175507j:plain
例えば変数が状態を持ってしまうと、その変数を参照する箇所で「この変数の今の値は何だろう?」と注意しなければならない。

f:id:zecl:20150928092712j:plain
この例においては、2番目の出力は「5963」ではなく「4649」と表示される。

F#は、変数を後から宣言した同名の変数でシャドウイング(隠ぺい)することができる。シャドウィングが使えることで、破壊的代入をしなくても不便さはそれほどなくなる。また、プログラムのスコープが明確になり、書きやすいだけではなく非常に読みやすいプログラムとなる。

f:id:zecl:20150928092752j:plain
immutableな値として、同名の変数として宣言するシャドウィングを用いる場合、 スコープの中の直前のものにのみ着目すればよいので、余計な複雑さを排除できてコードの見通しがくなり、プログラミングそのものに集中できる。 ※同一モジュール上のlet 宣言をシャドウィングすることはできないという制限あり

f:id:zecl:20150927180036j:plain
プログラマーのかわりに関数を利用しているコードの型などから、コンパイラがよしなに型をつけてくれる型推論。関数の引数や戻り値にも型を書かなくてよいし、可能であればジェネリックな関数として自動的に推論してくれたりする。 変数の定義も関数の定義も、letというキーワードで統一的に定義できるようになっていてうれしい。

少し乱暴だが、F#では let キーワードを覚えるだけで、かなりのプログラミングが書ける。match式あるいは if 式を覚えれば条件分岐もできる。 くわえて 再帰のための let rec を覚えれば、ループ処理が書けるので、いとも簡単に三種の神器を手に入れることができる(アッハイ)

f:id:zecl:20150927181717j:plain

f:id:zecl:20150927181745j:plain
JavaScriptのコードでカリー化関数と部分適用の説明。 突然のJavaScript!なのだが、社内勉強会用に作った資料の流用だったりするので。

f:id:zecl:20150927181853j:plain
f:id:zecl:20150927182004j:plain
F# ではカジュアルに無意識にカリー化関数が書けるので、うれしいという話

f:id:zecl:20150927182101j:plain
高階関数のうれしいところ

f:id:zecl:20150927182147j:plain
高階関数を扱いやすくするのに必要な機能とかの話。 F#では関数内関数が簡単にかけてうれしい。

f:id:zecl:20150927182253j:plain
高階関数を書きやすくする要因のひとつとして、関数内関数が無理なくかけるかというのがある。

C# でも書けないこともないが、どうしても書きにくいので書くモチベーションにならない。 すすんで書くようなものではないことは多くの人が知っているし、C# では素直に名前を付けてメソッドとして切り出して書くべきだ。

F#では、無理なく関数内関数を書くことができるのでなんの抵抗も生まれないし、その場限りの関数であれば、関数内関数として定義することはごくごく当たり前に行われる。これは、インデントによるオフサイドルールとも相性が良く、書き方/スタイルとして非常になじむ。関数プログラミングを支援/推奨している言語では プログラミングスタイルそのものが変わります。

f:id:zecl:20150927183756j:plain
関数の合成のしやすさも、関数プログラミングのしやすさの指標と考えられます。C#で合成関数を書こうとするとかなり骨が折れます。しかもこれは、特定の型に関する合成しかできません。

F#で定義した関数は、コンパイラ型推論と自動ジェネリック化のメカニズムによって、可能であれば暗黙的にジェネリックな関数となります。これにより非常にシンプルな記述で、あらゆる型の関数を合成するような関数(演算子)が表現できてしまうというわけです。こういった特徴を兼ね備えているので、小さな関数を巧みに組み合わせて、より大きな関数を構築していくという関数プログラミングというプログラミングスタイルが違和感なく行えます。

f:id:zecl:20150927184323j:plain
レキシカルスコープのほうが、コードを追いやすい話。 ダイナミックスコープと言えば、Lisp とか?

f:id:zecl:20150927184502j:plain
仮にダイナミックなスコープだとすると、10 が出力される ダイナミックスコープで解決されてうれしい場面ってどの程度あるのだろうか。罠でしかない感。

F#はレキシカルスコープを採用しているので、オフサイドルールやシャドウィングの仕様ともよくマッチしていて人道的でわかりやすいスコープとなっている。なので F# のコードは非常に読みやすい(追いやすい)。これが仮にダイナミックなスコープだとすると、関数を適用したときに、どのような結果をもたらすかコードリーディングだけで判別しにくくなってしまう。高階関数とかこわくて使えない。

f:id:zecl:20150927185203j:plain
高階関数の利用を容易にする条件はこんな感じかな 言い換えると、関数プログラミングしやすい条件ともいえる。


ここから、「続きはWebで」のぶぶん
f:id:zecl:20150927185552j:plain
有名なアレ。 VBの方ですか?Nothingもっとこわい?

f:id:zecl:20150927185713j:plain
C#でも null非許容型が欲しいなんて話はたびたび聞きます。 テストでカバーすればなんとかなる? そういう話ではないんです。

f:id:zecl:20150927185818j:plain
F# では通常、null 値を値にも変数にも使用しません。デフォルトで危険性を回避するように設計されています。例外的な場合を除いて、nullの使用はできる限り避けるよう言語設計されています。

f:id:zecl:20150927190029j:plain
通常F#で定義されているクラスには null は代入できません。 ムリヤリつっこうもとしてもコンパイルエラーとなります。

f:id:zecl:20150927190144j:plain
そうは言っても、.NET Frameworkの上にある以上 F# でも null 値を扱うことはあります。 ただし、その状況はとても限られていて

  • .NET API を呼び出して引数として null を渡す場合
  • .NET のメソッド呼び出しからの戻り値または出力パラメーターを解釈する場合

のいずれかしかありません。これらの状況に対しては AllowNullLiteral 属性を使用して対応します。
これを使用することで null 許容する型を明示的に定義することができます。

この例ではその2つの状況いずれにも当てはまらないので、 AllowNullLiteral は使用すべきではありません。
また、null チェックは強制することができないので、チェック漏れがあれば当然 NullReferenceExceptionとなります。

ちなみに、F#で定義されていてF#からのみ使用する型の場合、null 値を作成する方法は、 Unchecked.defaultof または Array.zeroCreate のいずれかの関数を使用した場合に限られます。



f:id:zecl:20150927190535j:plain
とはいえ、値がない状態というのをプログラミングで表現したいシーンはたくさんあります。そんなときに役立つのがオプション型で、これにより「値がないかもしれない」ということを型として明示することができます。計算不能や計算失敗などにより、実際の値が存在しない場合などに使えます。null の厄介な点は null であることが型の情報として現れないことで。nullは参照型であればどんな型の値にもなり得るので、 null参照によるエラーというのは実行してみるまで誰にも分からないからです。つまり、nullが来るかどうかの判定をプログラマーが正確に管理しなければならないこと意味します。これは実際難しいことです。

f:id:zecl:20150927190812j:plain
オプション型を導入することで、 null値を使用することなく安全に「値がない状態」を表すことができます。

f:id:zecl:20150927190935j:plain
関数プログラミングとは直接は関係ないところですが、F# のとても面白い(エキサイティングな)機能なのでご紹介します。

f:id:zecl:20150927191038j:plain
「インフォメーションリッチプログラミングから LINQ を引いて残った物が 型プロバイダーだよ。」「わけがわからないよ。」

TypeProviderのドキュメントによると、「基本的に定型のデータあるいはサービス情報空間をF#プログラミング内に導入することを目的としています 」とのこと。コンパイル時に型のない情報に型を与えることで、F# コード上であらゆる情報を安全に扱えるようにする仕組み。

f:id:zecl:20150927191108j:plain
型プロバイダーの主目的はコンパイル時プログラミングではないものの、型のないものを扱う際に生じやすいしょーもないバグを未然に防ぐことができるソリューションと解釈することもできます。型のないものを安全に扱える世界があなたの手中にといったところです。

f:id:zecl:20150927192212j:plain
ごむごむ

f:id:zecl:20150927214004j:plain
いろいろできそうです(オラワクワクしてきたぞ)

でも、TypeProviderはDSLなどをメタプログラミングするためのものではないと、TypeProviderのドキュメントに書かれています。
ですがが、まぁ無視してしまって全然よい気はしますね(実際にさまざまなTypeProviderでかなり無視されている) 特に学習目的であればメタプログラミング目的の方がとっつきやすいかもしれませんので。



f:id:zecl:20150927192302j:plain
NuGetからTypeProviders.StarterPack を導入するのがもっとも近道。 TypeProviderを作る際に必要な便利部品がひととおり用意されている。

f:id:zecl:20150927192351j:plain
スキーマが動的に変化することは想定されていないので、スキーマがコーディング中やプログラムの実行中に安定しているかということは、もちろん前提として必要。

ここで言う .NET の型システムへのマッピングとは、つまりクラス だったり 構造体だったりに対するマッピング。F# の型システムと言っているのは、主に判別共用体やレコード型に対するマッピングを意味している。関数プログラミングとの親和性を考えて、F# の型システムの型を生成するのか、あるいはオブジェクト指向プログラミングになじみやすいような型を生成するのか、そのあたりはよく検討してデザインする必要がある。

関数プログラミングとの親和性をとる場合の例でいうと、判別共用体は再帰的な構造を表現するのにとても適正しているので、冒頭で紹介した弾幕記述言語の F# 実装であるところの FsBulletMlでは実際に、XML(外部DSL)から判別共用体(型付き内部DSL)への安全なマッピングのためのTypeProvider提供しています。

f:id:zecl:20150927193158j:plain
セッションでは、いちおう簡単なDemoだけ紹介しました。



f:id:zecl:20150927194029p:plain

f:id:zecl:20150927194037p:plain
コンパイル時にメタデータからリアルタイムで型がつくられる。Visual Studioとも連携しているので、インテリセンスも効くという感じ。
こんな簡単なDemoからでも、「TypeProviderでなんか面白いことができそうだな!」ということが容易に想像できると思う。


f:id:zecl:20150927194247j:plain
今回のDemoのような単純なものであれば少し勉強すれば、すぐに作れるようになりますが、ある程度複雑なものになると、ぐんと難しくなります。
ただ、それは関数プログラミング的な側面で難しいという意味ではなく、メタプログラミングの側面として難しいという意味です。

TypeProviderの作り方について学ぶには、日本語情報に限らず海外の情報もまだまだ少ないので、 FSharp.Data等のライブラリのソースを読んで勉強する感じになるかと思います。 github.com

f:id:zecl:20150927194634j:plain

f:id:zecl:20150927194644j:plain
F#に限らずさまざまな言語の話も飛び交う感じの割とフリーダムなゆるい集まり。
私も何度かお邪魔して楽しくすごさせていただいています。ほとんどF#を書いたことがないというような人でも気軽に参加していただける勉強会です。
F#に詳しい参加者も多いので、やさしいF#erにマンツーマンでわからないことをいろいろ教えてもらえる(かも!?)

f:id:zecl:20150927220102j:plain
日本全国のF#er の集まり(online)という位置づけ(のはず)

gitterで F# に関する情報交換などがなされています gitter.im



f:id:zecl:20150927194719j:plain
英語ですが、F#に関するさまざまな情報がまとまっています(無料会員登録可) 特に言語の発展に物申したい場合は有料会員になる感じだと思います。 F# Software Foundation

@yukitos さんは、こちらのボードメンバーでもあります。

f:id:zecl:20150927195313j:plain
f:id:zecl:20150927195323j:plain
他のプログラミング言語に比べてF#が特別難しいというようなことはありません(もちろん難しい部分も当然あります)。

F#は高階関数が扱いやすく関数プログラミングがしやすい言語です。なので、プログラミングのスタイルは自然と関数プログラミングに軸をおいたものとなります。


よく、F#って何に使えるんですか?等の質問をいただきますが、VBC#とほとんど同じ場面で使えると同じように、ほとんどの場面で同じようにF#も使えます。F#を選択する理由があるとするならば、関数プログラミングのスタイルおよびそれによって享受できるメリットを重視するかしないかくらいのものです。それ以上でもそれ以下でもありません。関数プログラミングのスタイルを重視しないのであれば、選ぶ理由はないでしょう。 現状、世間一般ではそれを重視しないのが多数派であり、人材の確保が難しかったりある程度学習コストが高いという側面があるので、選択肢としてあがることも少なく(んなもんで人材が増えないという悪循環)、結果「流行っていない」ということになります。

手続き寄りの一般的なプログラミングを習得している人に対して、抽象化の道具として新たに関数プログラミングを軸にすることを提案することは 実際間違いではないし、どちらかというと筋がよいことだと思うんだけど、それを必要としている人もいるししてない人もいるというのは理解できます。で、実際いまのところは(残念ながら)必要とされていない(らしい)ので、F#流行っていないということになります。大変良い言語(実際)ですが、今のところMSが推す気配なし!ということもあって、だいぶ闇は深いです。

f:id:zecl:20150927200510j:plain

長々と(ポエム)書きました(しゃべりました)が、少しでも興味を持ってもらえたなら、うれしいな #うれしいな

F# Build Tools for Unity(ゲームのやつ) - UniFSharpのご紹介

これは F# Advent Calendar 2014の延長戦、 30 日目の記事です。

f:id:zecl:20141229010705j:plain

書いたきっかけ

結局、25日に間に合いませんで。ゆるふわ #FsAdvent に急遽参加しました。そんなわけで、ML Advent Calendar 2014も合わせてどうぞ。 この記事は非常に誰得でニッチな内容を扱います。ほとんどの場合役には立たないでしょう。F# らしい成分もあまりありませんので、まあ適当に流してください。


UniFSharpとは?

UniFSharpは、私が無職だった(ニートしていた)ときに作成した Unityエディタ拡張 Assetです。割と簡単に導入することができます

f:id:zecl:20141229020738p:plain

Unityでのゲーム開発はMacで開発する方がなんとなく多い印象がありますが、わたしがVisual Studio使いたい勢ということもあり、こちらWindows専用となっています。Mac対応はそのうちするかも(?) というか、リポジトリ公開してますから、誰か適当にうまいことやっちゃってください。


上の動画の内容は少し古いですが、概要は伝わるかと。UniFSharpを使うと、Unityエディタ上のAssets/Create/F# Script/NewBehaviourScript のメニューから選択するだけで、F# Scriptを作成できます。 そして、Unityエディタ上でF# ScriptをDLLにビルドすることができます(MSBuild利用)。 Visual Studio(IDE)とも連携するよう実装しており、これにより F#(関数型プログラミング言語)でUnityのゲーム開発がしやすくなります。いわゆる、"作ろうと思えば作れるの知ってるけど、面倒くさくて誰もやらなかったことをやってみた系のツール"です。まぁ、実際やるといろいろ大変。また、オープンソースヒロインのユニティちゃん(ユニティ・テクノロジーズ・ジャパン提供)をマスコットキャラクターに採用しました。ユニティちゃんの音声で時報やイベント、ビルド結果の通知を受けられるという、本来の目的とはまったく関係のない機能も提供しています。


Unityを使うならまぁ当然 C# 一択です。がまぁ、趣味で使う分にはフリーダム。


1000 人 Unity ユーザがいるとすると、その中の 0.01 人は F# ユーザーかもね(適当)


ちなみに、UniFSharp自体も F# で書かれています(ちょっとC# Scriptが混ざってます)。そう、基本的には F#で Unity のほとんどの部分(エディタだろうがゲームだろうが)を書くことができます。この記事では、UniFSharpが提供する機能および、それがどのように実装されているのかについて書きます。ここで紹介でもしないと、GitHubリポジトリを誰も覗いてくれることもないでしょうし。ハイ。

ご利用の際は、まぁいろいろあると思います(お察し)。覚悟しましょう。

この記事を読むその前に...むろほしりょうたさんの初心者がF#をUnityで使ってみた!という記事をオススメします。


F# Scriptの作成

Unityのエディタ拡張では、カスタムメニューを簡単に作ることができます。

f:id:zecl:20141224002558p:plain

f:id:zecl:20141224002627p:plain


F#で実装する場合、Moduleに定義した関数にMenuItem属性を付けるとよいでしょう。

  [<MenuItem("Assets/Create/F# Script/NewBehaviourScript",false, 70)>]
  let createNewBehaviourScript () = FSharpScriptCreateAsset.CreateFSharpScript "NewBehaviourScript.fs"

  [<MenuItem("Assets/Create/F# Script/NewModule", false, 71)>]
  let createNewModule () = FSharpScriptCreateAsset.CreateFSharpScript "NewModule.fs"

  [<MenuItem("Assets/Create/F# Script/", false, 80)>]
  let createSeparator () = ()

  [<MenuItem("Assets/Create/F# Script/NewTabWindow", false, 91)>]
  let createNewTabEditorWindow () = FSharpScriptCreateAsset.CreateFSharpScript "NewTabWindow.fs"

  [<MenuItem("Assets/Create/F# Script/", false, 100)>]
  let createSeparator2 () = ()

  [<MenuItem("Assets/Create/F# Script/more...", false, 101)>]
  let more () = MoreFSharpScriptWindow.ShowWindow()


UnityのEditorWindowは、ScriptableObjectインスタンス化されたものです。ShowUtilityメソッドを実行すると、必ず手前に表示し続け、タブとして扱えないウィンドウを作れます。C# で作る場合と基本的に同じです。難しくないですね。以下のウィンドウでは、選択されたテンプレートファイルを元に、F# Scriptを生成するという機能を提供しています。

f:id:zecl:20141224002702p:plain


namespace UniFSharp
open System.IO
open UnityEditor
open UnityEngine

type MoreFSharpScriptWindow () =
  inherit EditorWindow ()
  [<DefaultValue>]val mutable index : int

  static member ShowWindow() = 
    let window = ScriptableObject.CreateInstance<MoreFSharpScriptWindow>()
    window.title <- FSharpBuildTools.ToolName + " - F# Script"
    window.ShowUtility()

  member this.OnGUI() =

    let scripts = this.GetFSharpScript()
    this.index <- EditorGUILayout.Popup(this.index, scripts)
    if GUILayout.Button("Create") then
      let fileName = scripts.[this.index] 
      FSharpScriptCreateAsset.CreateFSharpScript fileName

  member this.GetFSharpScript () : string array = 
    Directory.GetFiles(FSharpBuildTools.fsharpScriptTemplatePath, FSharpBuildTools.txtExtensionWildcard)
    |> Array.map (fun x -> Path.GetFileName(x).Replace(Path.GetExtension(x),""))


F# Scriptのテンプレートの例

namespace #RootNamespace#
open UnityEngine

type #ClassName# () =
    inherit MonoBehaviour()
    [<DefaultValue>] val mutable text : string
    member public this.Start () = "start..." |> Debug.Log
    member public this.Update () = "update..." + this.text |> Debug.Log


テンプレートファイルを元に F# Script ファイルを生成したら、その生成したファイルを Unity エディタに Asset として認識させる必要があります。認識をさせないと、Unity の Projectウィンドウ上に表示されません。Assetとして登録する場合、F# Scriptファイルの名前の編集が確定したタイミングで行うようにします。EndNameEditActionクラスを継承し、Actionメソッドをオーバーライドして実装します。AssetDatabase.LoadAssetAtPathで、F# ScriptをUnityEngine.Objectとして読み込み、ProjectWindowUtil.ShowCreatedAssetで、Projetウィンドウ上に表示させることができます。

f:id:zecl:20141224002941p:plain


type FSharpScriptCreateAsset () =
  inherit EndNameEditAction ()

  static member CreateScript defaultName templatePath =
    let directoryName = 
      let assetPath = AssetDatabase.GetAssetPath(Selection.activeObject)
      if String.IsNullOrEmpty (assetPath |> Path.GetExtension) then assetPath
      else assetPath |> getDirectoryName
    if fsharpScriptCeatable directoryName |> not then
      EditorUtility.DisplayDialog("Warning", "Folder name that contains the F# Script file,\n must be unique in the entire F# Project.", "OK") |> ignore
    else
      let icon = Resources.LoadAssetAtPath(FSharpBuildTools.fsharpIconPath, typeof<Texture2D>) :?> Texture2D
      ProjectWindowUtil.StartNameEditingIfProjectWindowExists(0, ScriptableObject.CreateInstance<FSharpScriptCreateAsset>(), defaultName, icon, templatePath)

  static member CreateFSharpScript fileName = 
    let tempFilePath = FSharpBuildTools.fsharpScriptTemplatePath + fileName + FSharpBuildTools.txtExtension
    FSharpScriptCreateAsset.CreateScript fileName (tempFilePath)

  override this.Action(instanceId:int, pathName:string, resourceFile:string) = 
    use sr = new StreamReader(resourceFile, new UTF8Encoding(false))
    use sw = File.CreateText(pathName)
    let filename = Path.GetFileNameWithoutExtension(pathName).Replace(" ","")

    let guid () = System.Guid.NewGuid() |> string
    let text = Regex.Replace(sr.ReadToEnd(), "#ClassName#", filename)
                |> fun text -> Regex.Replace(text, "#ModuleName#", filename)
                |> fun text -> Regex.Replace(text, "#RootNamespace#", FSharpProject.templateRootNamespace pathName)
                |> fun text -> Regex.Replace(text, "#AssemblyName#", FSharpProject.templateAssemblyName pathName)
                |> fun text -> Regex.Replace(text, "#Guid#", guid())
    sw.Write(text)
    AssetDatabase.ImportAsset(pathName)
    let uo = AssetDatabase.LoadAssetAtPath(pathName, typeof<UnityEngine.Object>)
    ProjectWindowUtil.ShowCreatedAsset(uo)


ちなみに、Visual F# Power Tools(VFPT)では、フォルダ名はプロジェクト全体で一意である必要があるので、UnityのProjectウィンドウ上で階層をフリーダムに作られると厄介なので、そのあたりの階層構造も一応 チェックしていたりという感じです。変な階層を作られると、.fsprojファイルがぶっ壊れて開けなくなっちゃいますからね。




Inspectorで F# コードのプレビューを表示

Inspectorウィンドウで F#コードのプレビューを表示するためには、カスタムエディタを作成します。ただし、カスタムエディタはDLLのみでは実装を完結することができないため(謎の制約)、C# Scriptで。 http://forum.unity3d.com/threads/editor-script-dll-and-regular-script-dll-not-adding-custominspector-scripts.107720/

f:id:zecl:20141224002941p:plain

using System.IO;
using UnityEditor;
using UnityEngine;
using UniFSharp;
using Microsoft.FSharp.Core;

[CustomEditor(typeof(UnityEngine.Object), true)]
public class FSharpScriptInspector : Editor
{
    private string code;
    void OnEnable()
    {
        Repaint();
    }

    public override void OnInteractivePreviewGUI(Rect r, GUIStyle background)
    {
        base.OnInteractivePreviewGUI(r, background);
    }

    public override void OnInspectorGUI()
    {
        GUI.enabled = true;

        if (!AssetDatabase.GetAssetPath(Selection.activeObject).EndsWith(".fs"))
        {
            DrawDefaultInspector();
        }
        else
        {
            EditorGUILayout.BeginHorizontal("box");
            GUIStyle boldtext = new GUIStyle();
            boldtext.fontStyle = FontStyle.Bold;
            EditorGUILayout.LabelField("Imported F# Script", boldtext);
            EditorGUILayout.EndHorizontal();

            var targetAssetPath = AssetDatabase.GetAssetPath(target);
            if (!Directory.Exists(targetAssetPath) && File.Exists(targetAssetPath))
            {
                var sr = File.OpenText(targetAssetPath);
                code = sr.ReadToEnd();
                sr.Close();

                GUIStyle myStyle = new GUIStyle();
                GUIStyle style = EditorStyles.textField;
                myStyle.border = style.border;
                myStyle.contentOffset = style.contentOffset;
                myStyle.normal.background = style.normal.background;
                myStyle.padding = style.padding;
                myStyle.wordWrap = true;
                EditorGUILayout.LabelField(code, myStyle);
            }

            var rec = EditorGUILayout.BeginHorizontal();
            if (GUI.Button(new Rect(rec.width - 80, 25, 50, 15), "vs-sln", EditorStyles.miniButton))
            {
                var path = AssetDatabase.GetAssetPath(Selection.activeObject);
                var basePath = FSharpProject.GetProjectRootPath();
                var fileName = PathUtilModule.GetAbsolutePath(basePath, path);
                UniFSharp.FSharpSolution.OpenExternalVisualStudio(SolutionType.FSharp, fileName);
            }

            if (GUI.Button(new Rect(rec.width - 145, 25, 60, 15), "mono-sln", EditorStyles.miniButton))
            {
                UniFSharp.FSharpSolution.OpenExternalMonoDevelop();
            }
            EditorGUILayout.EndHorizontal();
        }
    }
}


Editorを継承しOnInspectorGUIをオーバーライドし、Projectウィンドウで選択されたF# Scriptを読み込んで表示するよう実装します。雑ですが以上。

F# DLL のビルド

Unity上から MSBuildでビルドするだけの簡単なお仕事です。これといって特筆すべきことはありません。誰かソースきれいにして。

namespace UniFSharp
open System
open System.IO 
open System.Diagnostics 
open System.Text 
open System.Xml
open UnityEditor

[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module MSBuild =
  let private initOutputDir outputDirPath = 
    if (not <| Directory.Exists(outputDirPath)) then
      Directory.CreateDirectory(outputDirPath) |> ignore
    else
      Directory.GetFiles(outputDirPath) |> Seq.iter (fun file -> File.Delete(file))
      AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate)

  let private getAssemblyName (projectFilePath:string) = 
    let xdoc = new XmlDocument()
    xdoc.Load(projectFilePath)
    let xnm = new XmlNamespaceManager(xdoc.NameTable)
    xnm.AddNamespace("ns", "http://schemas.microsoft.com/developer/msbuild/2003")
    let node = xdoc.SelectSingleNode("/ns:Project/ns:PropertyGroup/ns:AssemblyName", xnm)
    let node = xdoc.SelectSingleNode("/ns:Project/ns:PropertyGroup/ns:AssemblyName")
    if (node = null) then ""
    else node.InnerText

  let private getAargs (projectFilePath:string) (outputDirPath:string) isDebug =
    let projectFilePath = projectFilePath |> replaceDirAltSepFromSep
    let outputDirPath = outputDirPath |> replaceDirAltSepFromSep

    // http://msdn.microsoft.com/ja-jp/library/bb629394.aspx
    let args = new StringBuilder()
    args.AppendFormat("\"{0}\"", projectFilePath)
        .AppendFormat(" /p:Configuration={0}", if isDebug then "Debug" else "Release")
        .AppendFormat(" /p:OutputPath=\"{0}\"", outputDirPath)
        .Append(" /p:OptionExplicit=true")
        .Append(" /p:OptionCompare=binary")
        .Append(" /p:OptionStrict=true")
        .Append(" /p:OptionInfer=true")
        .Append(" /p:BuildProjectReferences=false")
        .AppendFormat(" /p:DebugType={0}", if isDebug then "full" else "pdbonly")
        .AppendFormat(" /p:DebugSymbols={0}", if isDebug then "true" else "false")
        .AppendFormat(" /p:VisualStudioVersion={0}", "12.0") // TODO 
        //.AppendFormat("{0}", String.Format(" /p:DocumentationFile={0}/{1}.xml", outputDirPath, getAssemblyName projectFilePath))
        .AppendFormat(" /l:FileLogger,Microsoft.Build.Engine;logfile={0}", String.Format("{0}/{1}.log", outputDirPath, if isDebug then "DebugBuild" else "ReleaseBuild"))
        .Append(" /t:Clean;Rebuild")
        |> string

  let getMSBuildPath (version:string) = 
    let msBuildPath = (String.Format(@"SOFTWARE\Microsoft\MSBuild\{0}", version), @"MSBuildOverrideTasksPath") ||> UniFSharp.Registory.getReg
    Path.Combine(msBuildPath, "MSBuild.exe")

  let execute msBuildVersion projectFilePath outputDirPath isDebug outputDataReceivedEventHandler errorDataReceivedEventHandler = 
    use p = new Process()
    outputDirPath |> initOutputDir

    p.StartInfo.WindowStyle <- ProcessWindowStyle.Hidden
    p.StartInfo.CreateNoWindow <- true
    p.StartInfo.UseShellExecute <- true
    p.StartInfo.FileName <- getMSBuildPath msBuildVersion
    p.StartInfo.Arguments <- getAargs projectFilePath outputDirPath isDebug

    if (outputDataReceivedEventHandler = null |> not || errorDataReceivedEventHandler = null |> not) then
        p.StartInfo.UseShellExecute <- false
        p.StartInfo.CreateNoWindow <- true
        p.StartInfo.WindowStyle <- ProcessWindowStyle.Hidden

        if (outputDataReceivedEventHandler = null |> not) then
          p.StartInfo.RedirectStandardOutput <- true
          p.OutputDataReceived.AddHandler outputDataReceivedEventHandler

        if (errorDataReceivedEventHandler = null |> not) then
          p.StartInfo.RedirectStandardError <- true
          p.ErrorDataReceived.AddHandler errorDataReceivedEventHandler

    if p.Start() then
      if (outputDataReceivedEventHandler = null |> not) then
        p.BeginOutputReadLine()

      if (errorDataReceivedEventHandler = null |> not) then
        p.BeginErrorReadLine()

      p.WaitForExit()
      p.ExitCode
    else
      p.ExitCode 


UniFSharpでは、ビルドの結果をユニティちゃんが通知してくれます。ビルドエラーだとこんな感じ

f:id:zecl:20141224003402p:plain


F# Scriptのドラック&ドロップについて

「UniFSharp を使えば F# Script ファイルをUnity上で作れる」とは言っても、実際にScriptファイルとして動作するようには実装していないくて、実際はDLL化したアセンブリをUnityで読み込んで利用しているため、通常は Projectウィンドウに表示しているだけの F# Scriptファイルを、Inspectorウィンドウにドラック&ドロップしGameObjectにComponentとして追加することはできません。UniFSharpでは、アセンブリの内容を解析して、疑似的に F# Scriptファイルをドラッグ&ドロップしているかのような操作感覚を実現しています。

F# DLLとF# ScriptからMonoBehaviourの派生クラスを探索するモードは2種類用意していて、1つは、F# Scriptファイルを読み取って、シンプルな正規表現でクラス名を抽出し、アセンブリからMonoBehaviourの派生クラスを検索する方法。もう一つは、F# ScriptファイルをF# コンパイラサービスを利用して、解析して厳密にクラス名を抽出する方法。前者は精度は低いが早い。後者は精度は高いが遅い。それぞれ一長一短がある。

カスタムエディタということで、F# コンパイラサービスを利用する部分を除いては、またC#

using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;
using UniFSharp;
using System;
using System.Collections.Generic;
using System.Diagnostics;

[CustomEditor(typeof(UnityEngine.Transform))]
public class TransformInspector : Editor
{
    Vector3 position;

    void OnEnable()
    {
        Repaint();
    }

    public override void OnInspectorGUI()
    {
        EditorGUILayout.BeginVertical();
        (this.target as Transform).localRotation = Quaternion.Euler(EditorGUILayout.Vector3Field("Local Rotation", (this.target as Transform).localRotation.eulerAngles));
        (this.target as Transform).localPosition = EditorGUILayout.Vector3Field("Local Position", (this.target as Transform).localPosition);
        (this.target as Transform).localScale = EditorGUILayout.Vector3Field("Local Scale", (this.target as Transform).localScale);
        EditorGUILayout.EndVertical();

        // F# Script Drag % Drop
        if (DragAndDrop.objectReferences.Length > 0 && AssetDatabase.GetAssetPath(DragAndDrop.objectReferences[0]).EndsWith(".fs"))
        {
            DragDropArea<UnityEngine.Object>(null, draggedObjects => 
            {
                var dropTarget = this.target as Transform;


                foreach (var draggedObject in draggedObjects)
                {
                    var outputPath = FSharpProject.GetNormalOutputAssemblyPath();
                    if (!Directory.Exists(outputPath))
                    {
                        EditorUtility.DisplayDialog("Warning", "F# Assembly is not found.\nPlease Build.", "OK");
                        break;
                    }

                    var notfound = true;
                    foreach (var dll in Directory.GetFiles(outputPath, "*.dll"))
                    {
                        var fileName = Path.GetFileName(dll);
                        if (fileName == "FSharp.Core.dll") continue;

                        var assem = Assembly.LoadFrom(dll);
                        IEnumerable<Type> behaviors = null;
                        switch (UniFSharp.FSharpBuildToolsWindow.FSharpOption.assemblySearch)
                        {
                            case AssemblySearch.Simple:
                                var @namespace = GetNameSpace(AssetDatabase.GetAssetPath(draggedObject));
                                var typeName = GetTypeName(AssetDatabase.GetAssetPath(draggedObject));
                                behaviors = assem.GetTypes().Where(type => typeof(MonoBehaviour).IsAssignableFrom(type) && type.FullName == @namespace + typeName);
                                break;
                            case AssemblySearch.CompilerService:
                                var types = GetTypes(AssetDatabase.GetAssetPath(draggedObject));
                                behaviors = assem.GetTypes().Where(type => typeof(MonoBehaviour).IsAssignableFrom(type) && types.Contains(type.FullName));
                                break;
                            default:
                                 break;
                        }

                        if (behaviors != null && behaviors.Any())
                        {
                            DragAndDrop.AcceptDrag();
                            foreach (var behavior in behaviors)
                            {
                                dropTarget.gameObject.AddComponent(behavior);
                                notfound = false;
                            }
                        }
                    }

                    if (notfound)
                    {
                        EditorUtility.DisplayDialog("Warning", "MonoBehaviour is not found in the F # assembly.", "OK");
                        return;
                    }
                }
            }, null, 50);
        }
    }

    public static void DragDropArea<T>(string label, Action<IEnumerable<T>> onDrop, Action onMouseUp, float height = 50) where T : UnityEngine.Object
    {
        GUILayout.Space(15f);
        Rect dropArea = GUILayoutUtility.GetRect(0.0f, 50.0f, GUILayout.ExpandWidth(true));
        if (label != null) GUI.Box(dropArea, label);

        Event currentEvent = Event.current;
        if (!dropArea.Contains(currentEvent.mousePosition)) return;

        if (onMouseUp != null)
            if (currentEvent.type == EventType.MouseUp)
                onMouseUp();
        
        if (onDrop != null)
        {
            if (currentEvent.type == EventType.DragUpdated ||
                currentEvent.type == EventType.DragPerform)
            {
                DragAndDrop.visualMode = DragAndDropVisualMode.Copy;

                if (currentEvent.type == EventType.DragPerform)
                {
                    EditorGUIUtility.AddCursorRect(dropArea, MouseCursor.CustomCursor);
                    onDrop(DragAndDrop.objectReferences.OfType<T>());
                }
                Event.current.Use();
            }
        }
    }

    private string GetNameSpace(string path)
    {
        var @namespace = "";
        using (var sr = new StreamReader(path, new UTF8Encoding(false)))
        {
            var text = sr.ReadToEnd();
            string pattern = @"(?<![/]{2,})[\x01-\x7f]*namespace[\s]*(?<ns>.*?)\n";

            var re = new Regex(pattern, RegexOptions.IgnoreCase | RegexOptions.Singleline);
            foreach (Match m in re.Matches(text))
            {
                @namespace = m.Groups["ns"].Value.Trim() != "" ? m.Groups["ns"].Value.Trim() + "." : "";
                break;
            }
        }
        return @namespace;
    }

    private string GetTypeName(string path)
    {
        var typeName = "";
        using (var sr = new StreamReader(path, new UTF8Encoding(false)))
        {
            var text = sr.ReadToEnd();
            string pattern = @"(?<![/]{2,}\s{0,})type[\s]*(?<type>.*?)(?![\S\(\)\=\n])";
            var re = new Regex(pattern);
            foreach (Match m in re.Matches(text))
            {
                typeName = m.Groups["type"].Value.Trim();
                break;
            }
        }
        return typeName;
    }

    private string[] GetTypes(string path)
    {
        var path2 = UniFSharp.PathUtilModule.GetAbsolutePath(Application.dataPath, path);
        var p = new Process();
        p.StartInfo.FileName = FSharpBuildToolsModule.projectRootPath + @"Assembly\GN_merge.exe";
        p.StartInfo.Arguments = path2 + " " + "DEBUG";
        p.StartInfo.CreateNoWindow = true;
        p.StartInfo.UseShellExecute = false;
        p.StartInfo.RedirectStandardOutput = true;
        p.Start();
        p.WaitForExit();
        var outputString = p.StandardOutput.ReadToEnd();
        var types = outputString.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
        return types;
    }
}


F# コンパイラサービスを使って、F# Scriptファイルから名前空間を含むクラス名の探索はこんな感じ。

module Parser
open System
open Microsoft.FSharp.Compiler.SourceCodeServices
open Microsoft.FSharp.Compiler.Ast

let private checker = InteractiveChecker.Create()
let private getUntypedTree (file, input, conditionalDefines) = 
  let otherFlags = 
    match conditionalDefines with
    | [||] -> [||] 
    | _  -> conditionalDefines |> Array.map (fun x -> "--define:" + x )

  let checkOptions = checker.GetProjectOptionsFromScript(file, input, otherFlags = otherFlags) |> Async.RunSynchronously
  let untypedRes = checker.ParseFileInProject(file, input, checkOptions) |> Async.RunSynchronously
  match untypedRes.ParseTree with
  | Some tree -> tree
  | None -> failwith "failed to parse"

let rec private getAllFullNameOfType' modulesOrNss =
  modulesOrNss |> Seq.map(fun moduleOrNs -> 
    let (SynModuleOrNamespace(lid, isModule, moduleDecls, xmlDoc, attribs, synAccess, m)) = moduleOrNs
    let topNamespaceOrModule = String.Join(".",(lid.Head::lid.Tail))
    //inner modules
    let modules = moduleDecls.Head::moduleDecls.Tail 
    getDeclarations modules |> Seq.map (fun x -> String.Join(".", [topNamespaceOrModule;x]))
    ) |> Seq.collect id

and private getDeclarations moduleDecls = 
  Seq.fold (fun acc declaration -> 
      match declaration with
      | SynModuleDecl.NestedModule(componentInfo, modules, _isContinuing, _range) ->
        match componentInfo with
        | SynComponentInfo.ComponentInfo(_,_,_,lid,_,_,_,_) ->
          let moduleName = String.Join(".",(lid.Head::lid.Tail))
          let children = getDeclarations modules
          seq {
            yield! acc
            yield! children |> Seq.map(fun child -> moduleName + "+" + child) }
      | SynModuleDecl.Types(typeDefs, _range) ->
        let types = 
          typeDefs |> Seq.map(fun typeDef ->
          match typeDef with
          | SynTypeDefn.TypeDefn(componentInfo,_,_,_) ->
          match componentInfo with
          | SynComponentInfo.ComponentInfo(_,typarDecls,_,lid,_,_,_,_) ->
            let typarString = typarDecls |> function | [] -> "" | x -> "`" + string x.Length 
            let typeName = String.Join(".",(lid.Head::lid.Tail))
            typeName + typarString)
        seq {
          yield! acc
          yield! types }
      | _ -> acc
    ) Seq.empty moduleDecls

let getAllFullNameOfType input conditionalDefines = 
  let tree = getUntypedTree("/dummy.fsx", input, conditionalDefines) 
  match tree with
  | ParsedInput.ImplFile(ParsedImplFileInput(file, isScript, qualName, pragmas, hashDirectives, modules, b)) ->
    getAllFullNameOfType' modules 
  | _ -> failwith "(*.fsi) not supported."


module Program
open System
open System.IO 
open Microsoft.FSharp.Compiler.Ast
open Parser

[<EntryPoint>]
let main argv = 
  let cmds = System.Environment.GetCommandLineArgs()
  if cmds.Length < 2 then 0 else

  let fileName = cmds.[1].Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar)
  let conditionalDefines = 
    if cmds.Length > 2 then cmds.[2].Split(';') 
    else [||]

  let input = File.ReadAllText(fileName)
  getAllFullNameOfType input conditionalDefines 
  |> Seq.iter(fun x -> printfn "%s" x)
  0


ところで、F# コンパイラサービスの対象フレームワーク.NET Framework4以上です。いまはまだ Unityでこのアセンブリを読み込むことはできません。残念!!なのですが、ここで、Microsoftが提供しているILMergeという神ツールを使う(苦肉の策)ことにより、それを回避し実現してる(アッハイ)。

F# Projectファイルの操作とVisual Studioとの連携

Unityエディタで F# Script を作成することをサポートしたということは、つまり、IDEとの連携もサポートするってことだよね。Projectウィンドウ上でF# Scriptファイルを追加したり、ファイルのパスを移動したり、ファイルを削除したタイミングで.fsprojファイル(XML) の内容が書き換わってくれないと、それぜーんぜん役に立たない。そういうこと。この実装がけっこー面倒くさかった...。

こんな感じ


Assetを追加・削除・移動した際に独自の処理をしたい場合は、AssetPostprocessorを継承して適宜処理を実装する。さらに、それがUnityで標準では扱われないファイルの場合(まさに今回の F# Scriptがこの場合)には、OnPostprocessAllAssetsメソッドを実装する。そこで .fsprojファイルをごにょごにょすることで、これを実現できる。

コードは、こんな雰囲気(あばばばば)

namespace UniFSharp
open System
open System.IO
open System.Linq
open System.Xml.Linq
open UnityEditor
open UnityEngine

type FSharpScriptAssetPostprocessor () = 
  inherit AssetPostprocessor ()
  static let ( !! ) s = XName.op_Implicit s

  static let getXDocCompileIncureds (fsprojXDoc:XDocument) (ns:string) (projectFileType:ProjectFileType) =
    let elements = fsprojXDoc.Root.Elements(!!(ns + "ItemGroup")).Elements(!!(ns + "Compile"))
    elements |> Seq.map (fun x -> x.Attribute(!!"Include").Value |> replaceDirSepFromAltSep)

  static let getNewCompileIncludeElement(ns:string) (file:string) = XElement(!!(ns + "Compile"), new XAttribute(!!"Include", file))
  static let getNewItemGroupCompileIncludeElement (ns:string) (file:string) = XElement(!!(ns + "ItemGroup"), new XElement(!!(ns + "Compile"), new XAttribute(!!"Include", file)))
  static let getXDocComiles (fsprojXDoc:XDocument) (ns:string) = fsprojXDoc.Root.Elements(!!(ns + "ItemGroup")).Elements(!!(ns + "Compile"))

  static let getNotExitsFiles (compileIncludes:seq<string>) (projectFileType:ProjectFileType) =
    let basePath = FSharpProject.getProjectRootPath()
    let files = FSharpProject.getAllFSharpScriptAssets(projectFileType) 
                |> Seq.map (fun x -> getRelativePath basePath x)
    Seq.fold(fun acc file -> 
      let file = file |> replaceDirSepFromAltSep
      if not (compileIncludes |> Seq.exists ((=)file)) then 
        seq { yield! acc
              yield file } 
      else acc) Seq.empty files

  static let addCompileIncludeFiles (fsprojXDoc:XDocument) (ns:string) (compileIncludes:seq<string>) (projectFileType:ProjectFileType) =
    let notExists = getNotExitsFiles compileIncludes projectFileType
    notExists |> Seq.iter (fun file ->
      let newElem = getNewCompileIncludeElement ns file
      let compiles = getXDocComiles fsprojXDoc ns
      if (compiles.Any()) then
        let addPoint () =
          let directoryPoint = 
            compiles |> Seq.toList |> Seq.filter (fun x -> 
              let includeFile = x.Attribute(!!"Include").Value
              let includeDirectory = getDirectoryName(includeFile) |> replaceDirSepFromAltSep 
              let directory = getDirectoryName(file) |> replaceDirSepFromAltSep 
              includeDirectory = directory)

          if directoryPoint.Any() then
            directoryPoint |> Seq.toList
          else compiles|> Seq.toList
        addPoint().Last().AddAfterSelf(newElem)
      else
        let newItemGroupElem = getNewItemGroupCompileIncludeElement ns file
        fsprojXDoc.Root.Add(newItemGroupElem))

  static let getRemoveFiles (compileIncludes:seq<string>) (projectFileType:ProjectFileType) =
    let basePath = FSharpProject.getProjectRootPath()
    Seq.fold(fun acc ``include`` -> 
      let ``include`` = ``include`` |> replaceDirSepFromAltSep
      let files = FSharpProject.getAllFSharpScriptAssets(projectFileType) |> Seq.map (fun x -> getRelativePath basePath x) |> Seq.map (fun x -> x |> replaceDirSepFromAltSep)
      if (not <| files.Contains(``include``)) then 
        seq { yield! acc
              yield ``include`` } 
      else acc) Seq.empty compileIncludes

  static let removeCompileIncludeFiles (fsprojXDoc:XDocument) (ns:string) (compileIncludes:seq<string>) (projectFileType:ProjectFileType) =
    let removedFiles = getRemoveFiles compileIncludes projectFileType
    removedFiles |> Seq.iter (fun file -> 
      let compileItems = (fsprojXDoc.Root.Elements(!!(ns + "ItemGroup")).Elements(!!(ns + "Compile")))
      if compileItems |> Seq.length = 1 && (compileItems |> Seq.exists (fun x -> x.Attribute(!!"Include").Value = file)) then
        let parent = compileItems |> Seq.map(fun x -> x.Parent) |> Seq.head 
        parent.Remove()
      else
        (compileItems |> Seq.filter (fun x -> x.Attribute(!!"Include").Value = file)).Remove())    

  static let createOrUpdateProject (projectFileType:ProjectFileType) =
    let fsprojFileName = FSharpProject.getFSharpProjectFileName(projectFileType)
    let fsprojFilePath = FSharpProject.getFSharpProjectPath(fsprojFileName)
    if (not <| File.Exists(fsprojFilePath)) then
      FSharpProject.createFSharpProjectFile(projectFileType) |> ignore
    else
      let fsprojXDoc = XDocument.Load(fsprojFilePath)
      let ns = "{" + String.Format("{0}", fsprojXDoc.Root.Attribute(!!"xmlns").Value) + "}"
      let compileIncludes = getXDocCompileIncureds fsprojXDoc ns projectFileType
      addCompileIncludeFiles fsprojXDoc ns compileIncludes projectFileType
      removeCompileIncludeFiles fsprojXDoc ns compileIncludes projectFileType
      fsprojXDoc.Save(fsprojFilePath)

  static let deleteProject (projectFileType:ProjectFileType) (assetPath:string) =
    let assetPath = assetPath |> replaceDirSepFromAltSep 
    let fsprojFileName = FSharpProject.getFSharpProjectFileName projectFileType
    if (File.Exists(fsprojFileName)) then
      let basePath = FSharpProject.getProjectRootPath()
      let fsprojXDoc = XDocument.Load(fsprojFileName)
      let ns = "{" + String.Format("{0}", fsprojXDoc.Root.Attribute(!!"xmlns").Value) + "}"
      let compileIncludes = fsprojXDoc.Root
                                      .Elements(!!(ns + "ItemGroup"))
                                      .Elements(!!(ns + "Compile")) 
                            |> Seq.map (fun x -> x.Attribute(!!"Include").Value)
      let compileIncludes = compileIncludes |> Seq.map (fun x -> x |> replaceDirSepFromAltSep)
      fsprojXDoc.Root
                .Elements(!!(ns + "ItemGroup"))
                .Elements(!!(ns + "Compile")) 
                .Where(fun x -> x.Attribute(!!"Include").Value |> replaceDirSepFromAltSep = assetPath).Remove()
      fsprojXDoc.Save(fsprojFileName)
    else ()

  static let createOrUpdateEditor () =
    ProjectFileType.VisualStudioEditor |> createOrUpdateProject
    ProjectFileType.MonoDevelopEditor  |> createOrUpdateProject

  static let createOrUpdateNormal () = 
    ProjectFileType.VisualStudioNormal |> createOrUpdateProject
    ProjectFileType.MonoDevelopNormal  |> createOrUpdateProject

  static let createOrUpdate () = 
    createOrUpdateNormal()
    createOrUpdateEditor()

  static let filterFSharpScript x = x |> Seq.filter(fun assetPath -> Path.GetExtension(assetPath) = FSharpBuildTools.fsExtension)

  static let onImportedAssets(importedAssets) = 
    importedAssets |> filterFSharpScript |> fun _ -> createOrUpdate ()
    UniFSharp.FSharpSolution.CreateSolutionFile()

  static let onDeletedAssets(deletedAssets) = 
    deletedAssets |> filterFSharpScript
    |> Seq.iter (fun assetPath ->
      if (FSharpProject.containsEditorFolder assetPath) then
        deleteProject ProjectFileType.VisualStudioEditor assetPath
        deleteProject ProjectFileType.MonoDevelopEditor assetPath
      else
        deleteProject ProjectFileType.VisualStudioNormal assetPath
        deleteProject ProjectFileType.MonoDevelopNormal assetPath)

  static let onMovedAssets(movedAssets) = 
    movedAssets |> filterFSharpScript
    |> Seq.iter (fun assetPath ->
      let assetAbsolutePath = assetPath |> (getAbsolutePath Application.dataPath)
      let fileName = assetAbsolutePath |> Path.GetFileName 
      if fsharpScriptCeatable assetAbsolutePath |> not then
        EditorUtility.DisplayDialog("Warning", "Folder name that contains the F# Script file,\n must be unique in the entire F# Project.\nMove to Assets Folder.", "OK") |> ignore
        AssetDatabase.MoveAsset(assetPath, "Assets/" + fileName) |> ignore
        AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate))

  static let onMovedFromPathAssets(movedFromPath) = 
    if movedFromPath |> filterFSharpScript |> Seq.exists (fun _ -> true) then
      createOrUpdateNormal()
    
  static member OnPostprocessAllAssets (importedAssets:string array, deletedAssets:string array, movedAssets:string array, movedFromPath:string array) = 
    onImportedAssets importedAssets
    onDeletedAssets deletedAssets
    onMovedAssets movedAssets
    onMovedFromPathAssets movedFromPath


また、UnityのProjectウィンドウ上でF# Scriptをダブルクリックした際に、Visual Studio上でそのファイルをアクティブにする動作を実現するために、EnvDTEを利用した。 http://msdn.microsoft.com/ja-jp/library/envdte.dte.aspx


これはきな臭い...。UniFSharpがWindows専用であることが滲み出ているコードですね。はい(真顔)

namespace DTE
open System
open System.Linq 
open System.Runtime.InteropServices
open System.Runtime.InteropServices.ComTypes
open EnvDTE

module AutomateVisualStudio = 
  let is64BitProcess = (IntPtr.Size = 8)
  [<DllImport("kernel32.dll", SetLastError = true, CallingConvention = CallingConvention.Winapi)>]
  extern [<MarshalAs(UnmanagedType.Bool)>] bool IsWow64Process([<In>] IntPtr hProcess, [<Out>] bool& wow64Process)

  [<CompiledName "InternalCheckIsWow64">]
  let internalCheckIsWow64 () = 
    let internalCheckIsWow64 () = 
      if ((Environment.OSVersion.Version.Major = 5 && Environment.OSVersion.Version.Minor >= 1) || Environment.OSVersion.Version.Major >= 6) then
        use p = System.Diagnostics.Process.GetCurrentProcess()
        let mutable retVal = false
        if (not <| IsWow64Process(p.Handle, &retVal)) then
          false
        else
          retVal
      else
        false

    is64BitProcess || internalCheckIsWow64()

  [<CompiledName "Is64BitOperatingSystem">]
  let is64BitOperatingSystem = is64BitProcess || internalCheckIsWow64 ()

  [<CompiledName "GetVisualStudioInstallationPath">]
  let getVisualStudioInstallationPath (version:string) =
    let installationPath = 
      if (is64BitOperatingSystem) then
        Registory.getReg (String.Format(@"SOFTWARE\Wow6432Node\Microsoft\VisualStudio\{0}", version)) "InstallDir"
      else
        Registory.getReg (String.Format(@"SOFTWARE\Microsoft\VisualStudio\{0}", version)) "InstallDir"
    installationPath + "devenv.exe"

  let openExternalScriptEditor vsVersion solutionPath = 
    let p = new System.Diagnostics.Process()
    p.StartInfo.Arguments <- solutionPath
    p.StartInfo.FileName <- getVisualStudioInstallationPath vsVersion
    p.Start()

  [<DllImport("ole32.dll")>]
  extern int CreateBindCtx(uint32 reserved, [<Out>] IBindCtx& ppbc)

  let marshalReleaseComObject(objCom: obj) =
    let i = ref 1
    if (objCom <> null && Marshal.IsComObject(objCom)) then
      while (!i > 0) do
        i := Marshal.ReleaseComObject(objCom)

  let getDTE' (processId:int) (dteVersion:string) =
    let progId = String.Format("!VisualStudio.DTE.{0}:", dteVersion) + processId.ToString()
        
    let mutable bindCtx : IBindCtx = null;
    let mutable rot : IRunningObjectTable= null;
    let mutable enumMonikers :IEnumMoniker = null;
    let mutable runningObject : obj = null
    
    try
      Marshal.ThrowExceptionForHR(CreateBindCtx(0u, &bindCtx))
      bindCtx.GetRunningObjectTable(&rot)
      rot.EnumRunning(&enumMonikers)

      let moniker = Array.create<IMoniker>(1) null
      let numberFetched = IntPtr.Zero
      let cont' = ref true 
      while (enumMonikers.Next(1, moniker, numberFetched) = 0 && !cont') do
        let runningObjectMoniker = moniker.[0]
        let mutable name = null

        try
          if (runningObjectMoniker <> null) then
            runningObjectMoniker.GetDisplayName(bindCtx, null, &name)
        with | :? UnauthorizedAccessException -> () // do nothing

        if (not <| String.IsNullOrEmpty(name) && String.Equals(name, progId, StringComparison.Ordinal)) then
          Marshal.ThrowExceptionForHR(rot.GetObject(runningObjectMoniker, &runningObject))
          cont' := false
    finally
      if (enumMonikers <> null) then
        enumMonikers |> marshalReleaseComObject
      if (rot <> null) then
        rot |> marshalReleaseComObject
      if (bindCtx <> null) then
        bindCtx |> marshalReleaseComObject
    runningObject :?> EnvDTE.DTE

  let tryGetDTE (dteVersion:string) (targetSolutionFullName:string) tryMax =
    let getVisualStudioProcesses () =
      System.Diagnostics.Process.GetProcesses() |> Seq.where(fun x -> try x.ProcessName = "devenv" with | _  ->false)

    try
      let retry = RetryBuilder(tryMax,1.)
      retry {
        return 
          getVisualStudioProcesses() |> Seq.tryPick(fun p ->
            let dte = getDTE' p.Id dteVersion
            if (targetSolutionFullName.ToLower() = dte.Solution.FullName.ToLower()) then
              Some (dte,p)
            else
              None)}
    with | _ -> None

  let showDocument (dte:EnvDTE.DTE) (documentFullName:string) =
      let targetItem = 
        retry{
          let targetItem = dte.Solution.FindProjectItem(documentFullName)
          if (targetItem = null) then 
            return None 
          else
            return Some targetItem }

      match targetItem with
       | None -> ()
       | Some target ->
        if (not <| target.IsOpen(Constants.vsViewKindCode)) then
          target.Open(Constants.vsViewKindCode) |> ignore
          target.Document.Activate()
        else
          target.Document.Activate() 

  let jumpToLine dte documentFullName lineNumber =
    showDocument dte documentFullName
    let selectionDocument = dte.ActiveDocument.Selection :?> EnvDTE.TextSelection
    try
      selectionDocument.GotoLine(lineNumber, true) 
    with | _ -> () 


namespace DTE
open System
open System.Runtime.InteropServices
open System.IO
open Microsoft.Win32

[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module Registory =
  let getReg keyPath valueName = 
    try
      use rKey = Registry.LocalMachine.OpenSubKey(keyPath)
      let location = rKey.GetValue(valueName) |> string
      rKey.Close()
      location
    with e ->
      new Exception(String.Format("registry key:[{0}] value:[{1}] is not found.", keyPath, valueName)) |> raise


namespace DTE
open System
open System.IO
open AutomateVisualStudio

module Program =
   
  [<EntryPoint>]
  let main argv = 
    let cmds = System.Environment.GetCommandLineArgs()
    if cmds.Length < 4 then 0 else

    let vsVersion = cmds.[1] // "12.0"
    let solutionPath = cmds.[2].Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar)
    let targetDocumetFileName =  cmds.[3].Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar)

    if String.IsNullOrEmpty(vsVersion) then 0 else
    if File.Exists(solutionPath) |> not then 0 else
    if File.Exists(targetDocumetFileName) |> not then 0 else

    let active dte =
      if (cmds.Length = 4) then
        showDocument dte targetDocumetFileName
      else
        let lineNumber =  cmds.[4]
        if (lineNumber <> null) then
          let num = Int32.Parse(lineNumber)
          jumpToLine dte targetDocumetFileName num

    let dte = tryGetDTE vsVersion solutionPath 2
    match dte with
    | None -> 
      if openExternalScriptEditor vsVersion solutionPath then
        let dte = tryGetDTE vsVersion solutionPath 30
        dte |> Option.iter (fun (dte,p) -> active dte; Microsoft.VisualBasic.Interaction.AppActivate(p.Id))
    | Some (dte,p) -> 
      active dte
      Microsoft.VisualBasic.Interaction.AppActivate(p.Id)
    0


あと、Retryビルダー。アッハイ。モナドじゃねえっス。

namespace DTE
open System.Threading

[<AutoOpen>]
module Retry =

  type RetryBuilder(count, seconds) = 
    member x.Return(a) = a
    member x.Delay(f) = f
    member x.Zero() = failwith "Zero" 
    member x.Run(f) =
      let rec loop(n) = 
        if n = 0 then 
          failwith "retry failed"
        else 
          try 
            f()
          with e ->
            Thread.Sleep(seconds * 1000. |> int) 
            loop(n-1)

      loop count

  let retry = RetryBuilder(30,1.)


UniFSharpのオプション画面

ユニティちゃんの背景が印象的な画面です。

f:id:zecl:20141224003556p:plain


このオプション画面で、作成するF# プロジェクトの構成の詳細を設定できます。細かい説明は省きます(雑。

ユニティちゃんの機能もろもろ

ユニティ・テクノロジーズ・ジャパンが無償で提供してくれているユニティちゃんのAsset に同封されている多彩な音声。せっかくあるので使ってみたい。特に「進捗どうですか?」とか使わない手はない。そういや、いわるゆる萌え系だとか痛い系のIDEって結構あるけど、しゃべる感じのやつってあんまりないよなぁ。とかいうのが一応実装動機ということで。

f:id:zecl:20141224003648p:plain


  • ・起動時ボイス(ON/OFF)
  • ・ビルド時ボイス(ON/OFF)
  • ・進捗どうですか?(ON/OFF, 通知間隔指定あり)
  • 時報通知のボイス(ON/OFF)
  • ・イベント通知のボイス(ON/OFF)
  • ・誕生日のお祝い(ON/OFF, 日付を指定)

f:id:zecl:20141224003918p:plain


F#でUnityゲーム開発する気はなくても、Unityをお使いの方で、ユニティちゃんに「進捗どうですか?」とか言われたい人は、まぁ使ってみてくださいという感じで(適当)。

open UnityEngine
open UnityEditor

[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module AudioUtil =

  [<CompiledName "PlayClip">]
  let playClip (clip:AudioClip) =
    let method' = 
      let unityEditorAssembly = typeof<AudioImporter>.Assembly
      let audioUtilClass = unityEditorAssembly.GetType("UnityEditor.AudioUtil")
      audioUtilClass.GetMethod(
          "PlayClip",
          BindingFlags.Static ||| BindingFlags.Public,
          null,
          [|typeof<AudioClip>|],
          null)
    method'.Invoke(null, [|clip|]) |> ignore

Unityエディタ上で、音声ファイルを再生したい系の人は上記のような感じのをひとつこさえておけば、ハカドルかもね。

f:id:zecl:20141224003821p:plain


MonoDevelop、Xamarin、Visual Studioで Unity の F# DLLデバッグ

UniFSharpとは直接は関係ありませんが、Unity で F# DLLをデバッグする方法も紹介しておきたい。

基本的には、Unity ユーザーマニュアルに書いてあるとおりにすればよいです。 Unity プロジェクトでの Mono DLL 使用 / Using Mono DLLs in a Unity Project

ということで、.fsproj のビルド後イベントに、下記のような感じで設定しておくと捗るかもしれません(パスとかは適当に変えて)。

if exist "..\..\Assets\Assembly-FSharp-Editor\$(TargetName).pdb" call "C:\Program Files (x86)\Unity\Editor\Data\Mono\lib\mono\2.0\pdb2mdb.exe" "..\..\Assets\Assembly-FSharp-Editor\$(TargetName).dll"


Visual Studio 2013 Tools for Unityが無償提供され、あらゆるアプリを開発できる最強の開発ツールとの触れ込みのVisual Studio 2013 Community Editionが無償提供されたことで、誰でもVisual Studio で Unityのデバッグ実行ができるようになりました。本当にいい世の中になったものです。F# をお使いなら、 Visual F# Power Toolsも利用できますし、めしうま状態必至。



おまけ

Unityえふしゃーぷまん達


意外といらっしゃる。もちろんこれですべてではない。

Unity は良くできているゲームエンジンなので、F# でも使いたい!という気持ちはわかりますが、一般的には、F# でゲームを作りたいなら MonoGame あたりを選択する方がかしこいんじゃないでしょうか。はい。とは言え、 身の回りにUnity F# マンがいたら、ぜひとも情報交換などしてみたいですね。

ところで、わたくしごとで恐縮ですが、8か月以上という長いニート期間を終え、 12/1 から株式会社グラニで働いております。みなさんご存じ「最先端のC#技術を使った」ゲーム開発をしている会社です。とてもよい環境で仕事をさせていただいています。ということでわたくし現在東京におりますので、F# 談話室がある際にはぜひ遊びに行きたいです。趣味のF#erからは以上です。

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

一応、型プロバイダー(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) のいずれかであるため、静的引数として渡すことができる。


 

open System
open Sample.Domain

type Test = StaticParametersSample<true,86uy,86y,86s,86us,86,86u,86L,86UL,'a',"a",0.7833M,86.0F,86.,ColorSbyte.Red,ColorByte.Green,ColorInt16.Blue,ColorUint16.Red, ColorInt32.Green, ColorUint32.Blue, ColorInt64.Red, ColorUint64.Green, ColorChar.Blue>

[<EntryPoint>]
let main argv = 
  let test = new Test()
  test.Value |> printfn "%A"

  Console.ReadKey () |> ignore
  0


  実行結果

(true, 86uy, 86y, 86s, 86us, 86, 86u, 86L, 86UL, 'a', "a", 0.7833M, 86.0f, 86.0,
 Red, Green, Blue, Red, Green, Blue, Red, Green, Blue)

 

namespace Sample.Domain

open System
open System.IO
open System.Reflection
open System.Linq
open Microsoft.FSharp.Core.CompilerServices
open ProviderImplementation.ProvidedTypes

type ColorSbyte =
   | Red = 0y
   | Green = 1y
   | Blue = 2y

type ColorByte =
   | Red = 0uy
   | Green = 1uy
   | Blue = 2uy

type ColorInt16 =
   | Red = 0s
   | Green = 1s
   | Blue = 2s

type ColorUint16 =
   | Red = 0us
   | Green = 1us
   | Blue = 2us

type ColorInt32 =
   | Red = 0
   | Green = 1
   | Blue = 2

type ColorUint32 =
   | Red = 0u
   | Green = 1u
   | Blue = 2u

type ColorInt64 =
   | Red = 0L
   | Green = 1L
   | Blue = 2L

type ColorUint64 =
   | Red = 0UL
   | Green = 1UL
   | Blue = 2UL

type ColorChar =
   | Red = 'a'
   | Green = 'b'
   | Blue = 'c'

[<AutoOpen>]
module EnumExtentions = 
  let enum<'a,'b when 'b : enum<'a>> x = Microsoft.FSharp.Core.LanguagePrimitives.EnumOfValue<'a, 'b >(x)

[<TypeProvider>] 
type public StaticParametersSampleTypeProvider () as this = 
  inherit TypeProviderForNamespaces ()
  let asm = Assembly.GetExecutingAssembly()
  let ns = "Sample.Domain"

  let typ = ProvidedTypeDefinition(asm, ns, "StaticParametersSample", Some (typeof<obj>), HideObjectMethods = true, IsErased = true)
  do
    let parameters = 
      [ProvidedStaticParameter("bool", typeof<bool>)
       ProvidedStaticParameter("byte", typeof<byte>)
       ProvidedStaticParameter("sbyte", typeof<sbyte>)
       ProvidedStaticParameter("int16", typeof<int16>)
       ProvidedStaticParameter("uint16", typeof<uint16>)
       ProvidedStaticParameter("int", typeof<int>)
       ProvidedStaticParameter("uint32", typeof<uint32>)
       ProvidedStaticParameter("int64", typeof<int64>)
       ProvidedStaticParameter("uint64", typeof<uint64>)
       ProvidedStaticParameter("char", typeof<char>)
       ProvidedStaticParameter("string", typeof<string>)
       ProvidedStaticParameter("decimal", typeof<decimal>)
       ProvidedStaticParameter("float32", typeof<float32>)
       ProvidedStaticParameter("float", typeof<float>)
       ProvidedStaticParameter("ColorSbyte", typeof<ColorSbyte>)
       ProvidedStaticParameter("ColorByte", typeof<ColorByte>)
       ProvidedStaticParameter("ColorInt16", typeof<ColorInt16>)
       ProvidedStaticParameter("ColorUint16", typeof<ColorUint16>)
       ProvidedStaticParameter("ColorInt32", typeof<ColorInt32>)
       ProvidedStaticParameter("ColorUint32", typeof<ColorUint32>)
       ProvidedStaticParameter("ColorInt64", typeof<ColorInt64>)
       ProvidedStaticParameter("ColorUint64", typeof<ColorUint64>)
       ProvidedStaticParameter("ColorChar", typeof<ColorChar>)]

    typ.DefineStaticParameters(
        parameters,
        fun typeName parameters ->
          match parameters with
          | [| :?bool as pBool
               :?byte as pByte
               :?sbyte as pSbyte
               :?int16 as pInt16
               :?uint16 as pUint16
               :?int as pInt32
               :?uint32 as pUint32
               :?int64 as pInt64
               :?uint64 as pUint64
               :?char as pChar
               :?string as pString
               :?decimal as pDecimal
               :?float32 as pSingle
               :?float as pDouble
               :?sbyte as pColorSbyte
               :?byte as pColorByte
               :?int16 as pColorInt16
               :?uint16 as pColorUint16
               :?int as pColorInt32
               :?uint32 as pColorUint32
               :?int64 as pColorInt64
               :?uint64 as pColorUint64
               :?char as pColorChar
             |] -> 
            let typ = ProvidedTypeDefinition(asm, ns, typeName, Some typeof<obj>, HideObjectMethods = true, IsErased = true)
            let ctor = ProvidedConstructor(parameters = [ ], InvokeCode= (fun _ -> <@@ () @@>))
            typ.AddMember ctor
            typ.AddMemberDelayed(fun () ->
              let value = 
                <@@ pBool,
                    pByte, 
                    pSbyte, 
                    pInt16, 
                    pUint16, 
                    pInt32, 
                    pUint32, 
                    pInt64, 
                    pUint64, 
                    pChar, 
                    pString, 
                    pDecimal, 
                    pSingle, 
                    pDouble, 
                    enum<sbyte, ColorSbyte> pColorSbyte, 
                    enum<byte, ColorByte> pColorByte,
                    enum<int16, ColorInt16> pColorInt16, 
                    enum<uint16, ColorUint16> pColorUint16, 
                    enum<int, ColorInt32> pColorInt32,
                    enum<uint32, ColorUint32> pColorUint32, 
                    enum<int64, ColorInt64> pColorInt64, 
                    enum<uint64, ColorUint64> pColorUint64, 
                    enum<char, ColorChar> pColorChar 
                    @@>
              let instanceProp = 
                ProvidedProperty
                  (propertyName = "Value", 
                   propertyType = 
                     typeof<bool * 
                            byte * 
                            sbyte * 
                            int16 *
                            uint16 *
                            int *
                            uint32 *
                            int64 *
                            uint64 *
                            char *
                            string *
                            decimal *
                            float32 *
                            float * 
                            ColorSbyte *
                            ColorByte *
                            ColorInt16 *
                            ColorUint16 *
                            ColorInt32 *
                            ColorUint32 *
                            ColorInt64 *
                            ColorUint64 *
                            ColorChar
                            >, 
                                 GetterCode= (fun _ -> value))
              instanceProp.AddXmlDocDelayed(fun () -> 
                sprintf "<summary><para>%A</para></summary>" <|
                   (pBool, 
                    pByte, 
                    pSbyte, 
                    pInt16, 
                    pUint16, 
                    pInt32, 
                    pUint32, 
                    pInt64, 
                    pUint64, 
                    pChar, 
                    pString, 
                    pDecimal, 
                    pSingle, 
                    pDouble, 
                    enum<sbyte, ColorSbyte> pColorSbyte, 
                    enum<byte, ColorByte> pColorByte,
                    enum<int16, ColorInt16> pColorInt16, 
                    enum<uint16, ColorUint16> pColorUint16, 
                    enum<int, ColorInt32> pColorInt32,
                    enum<uint32, ColorUint32> pColorUint32, 
                    enum<int64, ColorInt64> pColorInt64, 
                    enum<uint64, ColorUint64> pColorUint64, 
                    enum<char, ColorChar> pColorChar ))
              instanceProp)
            typ
          | _ -> failwith "Invalid parameter" )

    this.AddNamespace(ns, [typ])

[<assembly:TypeProviderAssembly>] 
do()

 

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


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

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

たとえば、

namespace Sample.Domain

open System
open System.IO
open System.Linq
open System.Reflection
open Microsoft.FSharp.Core.CompilerServices
open ProviderImplementation.ProvidedTypes

type Hoge =
  | Fuga of string
  | Piyo of int

[<TypeProvider>] 
type public Sample1ErasedTypeProvider(cfg:TypeProviderConfig) as this = 
  inherit TypeProviderForNamespaces()
  let asm = Assembly.GetExecutingAssembly()
  let ns = "Sample.Domain"
  let parameters = [ProvidedStaticParameter("source", typeof<string>)]

  let typ = ProvidedTypeDefinition(asm, ns, "Sample1", Some (typeof<obj>), HideObjectMethods = true, IsErased = true)
  do
    typ.DefineStaticParameters(
        parameters,
        fun typeName parameters ->
          let source = string parameters.[0]
          let typ = ProvidedTypeDefinition(asm, ns, typeName, Some typeof<obj>, HideObjectMethods = true, IsErased = true)
          let ctor = ProvidedConstructor(parameters = [ ], InvokeCode= (fun _ -> <@@ source @@>))
          typ.AddMember ctor
          let p,r = Int32.TryParse(source)
          typ.AddMemberDelayed(fun () ->
            let value = p |> function | true -> Hoge.Piyo r | _ -> Hoge.Fuga source
            let instanceProp = 
              ProvidedProperty(propertyName = "Value", 
                               propertyType = typeof<Hoge>, 
                               GetterCode= (fun _ -> <@@ value @@>))
            instanceProp.AddXmlDocDelayed(fun () -> sprintf "<summary><para>%A</para></summary>" value)
            instanceProp)
          typ)
    this.AddNamespace(ns, [typ])

[<assembly:TypeProviderAssembly>] 
do()

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

f:id:zecl:20140825175254p:plain


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

namespace Sample.Domain

open System
open System.IO
open System.Linq
open System.Reflection
open Microsoft.FSharp.Core.CompilerServices
open ProviderImplementation.ProvidedTypes

[<TypeProvider>] 
type public Sample2ErasedTypeProvider(cfg:TypeProviderConfig) as this = 
  inherit TypeProviderForNamespaces()
  let asm = Assembly.GetExecutingAssembly()
  let ns = "Sample.Domain"
  let parameters = [ProvidedStaticParameter("source", typeof<string>)]

  let typ = ProvidedTypeDefinition(asm, ns, "Sample2", Some (typeof<obj>), HideObjectMethods = true, IsErased = true)
  do
    typ.DefineStaticParameters(
        parameters,
        fun typeName parameters ->
          let source = string parameters.[0]
          let typ = ProvidedTypeDefinition(asm, ns, typeName, Some typeof<obj>, HideObjectMethods = true, IsErased = true)
          let ctor = ProvidedConstructor(parameters = [ ], InvokeCode= (fun _ -> <@@ source @@>))
          typ.AddMember ctor
          let p,r = Int32.TryParse(source)
          typ.AddMemberDelayed(fun () ->
            let value = p |> function | true -> Hoge.Piyo r | _ -> Hoge.Fuga source
            let instanceProp = 
              ProvidedProperty(propertyName = "Value", 
                               propertyType = typeof<Hoge>, 
                               GetterCode= (fun _ -> <@@ p |> function | true -> Hoge.Piyo r | _ -> Hoge.Fuga source @@>))
            instanceProp.AddXmlDocDelayed(fun () -> sprintf "<summary><para>%A</para></summary>" value)
            instanceProp)
          typ)
    this.AddNamespace(ns, [typ])

[<assembly:TypeProviderAssembly>] 
do()

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


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

namespace Sample.Domain

open System
open System.IO
open System.Linq
open System.Reflection
open Microsoft.FSharp.Core.CompilerServices
open ProviderImplementation.ProvidedTypes
open Linq.QuotationEvaluation

[<TypeProvider>] 
type public Sample3ErasedTypeProvider(cfg:TypeProviderConfig) as this = 
  inherit TypeProviderForNamespaces()
  let asm = Assembly.GetExecutingAssembly()
  let ns = "Sample.Domain"
  let parameters = [ProvidedStaticParameter("source", typeof<string>)]

  let typ = ProvidedTypeDefinition(asm, ns, "Sample3", Some (typeof<obj>), HideObjectMethods = true, IsErased = true)
  do
    typ.DefineStaticParameters(
        parameters,
        fun typeName parameters ->
          let source = string parameters.[0]
          let typ = ProvidedTypeDefinition(asm, ns, typeName, Some typeof<obj>, HideObjectMethods = true, IsErased = true)
          let ctor = ProvidedConstructor(parameters = [ ], InvokeCode= (fun _ -> <@@ source @@>))
          typ.AddMember ctor
          let p,r = Int32.TryParse(source)
          typ.AddMemberDelayed(fun () ->
            let value = <@@ p |> function | true -> Hoge.Piyo r | _ -> Hoge.Fuga source @@>
            let instanceProp = 
              ProvidedProperty(propertyName = "Value", 
                               propertyType = typeof<Hoge>, 
                               GetterCode= (fun _ -> value))
            instanceProp.AddXmlDocDelayed(fun () -> sprintf "<summary><para>%A</para></summary>" (value.EvalUntyped()))
            instanceProp)
          typ)
    this.AddNamespace(ns, [typ])

[<assembly:TypeProviderAssembly>] 
do()

 

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

namespace Sample.Domain

open System
open System.IO
open System.Linq
open System.Reflection
open Microsoft.FSharp.Core.CompilerServices
open ProviderImplementation.ProvidedTypes

module internal Hogehoge =
  let f source = 
    let p,r = Int32.TryParse(source)
    p |> function | true -> Hoge.Piyo r | _ -> Hoge.Fuga source

[<TypeProvider>] 
type public Sample4ErasedTypeProvider(cfg:TypeProviderConfig) as this = 
  inherit TypeProviderForNamespaces()
  let asm = Assembly.GetExecutingAssembly()
  let ns = "Sample.Domain"
  let parameters = [ProvidedStaticParameter("source", typeof<string>)]

  let typ = ProvidedTypeDefinition(asm, ns, "Sample4", Some (typeof<obj>), HideObjectMethods = true, IsErased = true)
  do
    typ.DefineStaticParameters(
        parameters,
        fun typeName parameters ->
          let source = string parameters.[0]
          let typ = ProvidedTypeDefinition(asm, ns, typeName, Some typeof<obj>, HideObjectMethods = true, IsErased = true)
          let ctor = ProvidedConstructor(parameters = [ ], InvokeCode= (fun _ -> <@@ source @@>))
          typ.AddMember ctor
          typ.AddMemberDelayed(fun () ->
            let instanceProp = 
              ProvidedProperty(propertyName = "Value", 
                               propertyType = typeof<Hoge>, 
                               GetterCode= (fun _ -> <@@ Hogehoge.f source @@>))
            instanceProp.AddXmlDocDelayed(fun () -> sprintf "<summary><para>%A</para></summary>" (Hogehoge.f source))
            instanceProp)
          typ)
    this.AddNamespace(ns, [typ])

[<assembly:TypeProviderAssembly>] 
do()

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

f:id:zecl:20140825180549p:plain


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

f:id:zecl:20140825180421p:plain

 

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

[<CompilerMessage("hidden...", 13730, IsError = false, IsHidden = true)>]
module Hogehoge =
  let f source = 
    let p,r = Int32.TryParse(source)
    p |> function | true -> Hoge.Piyo r | _ -> Hoge.Fuga source

 
ところで、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 を参照した型プロバイダーを作る。

namespace Library1
open System

type Hoge =
  | Fuga of string
  | Piyo of int

module Fugafuga =
  let f source = 
    let p,r = Int32.TryParse(source)
    p |> function | true -> Hoge.Piyo r | _ -> Hoge.Fuga source


型プロバイダー

namespace Sample.Domain

open System
open System.IO
open System.Linq
open System.Reflection
open Microsoft.FSharp.Core.CompilerServices
open ProviderImplementation.ProvidedTypes
open Library1

[<TypeProvider>] 
type public Sample5ErasedTypeProvider(cfg:TypeProviderConfig) as this = 
  inherit TypeProviderForNamespaces()
  let asm = Assembly.GetExecutingAssembly()
  let ns = "Sample.Domain"
  let parameters = [ProvidedStaticParameter("source", typeof<string>)]

  let typ = ProvidedTypeDefinition(asm, ns, "Sample5", Some (typeof<obj>), HideObjectMethods = true, IsErased = true)
  do
    typ.DefineStaticParameters(
        parameters,
        fun typeName parameters ->
          let source = string parameters.[0]
          let typ = ProvidedTypeDefinition(asm, ns, typeName, Some typeof<obj>, HideObjectMethods = true, IsErased = true)
          let ctor = ProvidedConstructor(parameters = [ ], InvokeCode= (fun _ -> <@@ source @@>))
          typ.AddMember ctor
          typ.AddMemberDelayed(fun () ->
            let instanceProp = 
              ProvidedProperty(propertyName = "Value", 
                               propertyType = typeof<Hoge>, 
                               GetterCode= (fun _ -> <@@ Fugafuga.f source @@>))
            instanceProp.AddXmlDocDelayed(fun () -> sprintf "<summary><para>%A</para></summary>" (Fugafuga.f source))
            instanceProp)
          typ)
    this.AddNamespace(ns, [typ])

[<assembly:TypeProviderAssembly>] 
do()

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

f:id:zecl:20140825181323p:plain

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

namespace Sample.Domain

open System
open System.IO
open System.Linq
open System.Reflection
open Microsoft.FSharp.Core.CompilerServices
open ProviderImplementation.ProvidedTypes
open Library1

[<TypeProvider>] 
type public Sample5ErasedTypeProvider(cfg:TypeProviderConfig) as this = 
  inherit TypeProviderForNamespaces()
  let asm = Assembly.GetExecutingAssembly()
  let ns = "Sample.Domain"
  let parameters = [ProvidedStaticParameter("source", typeof<string>)]

  let typ = ProvidedTypeDefinition(asm, ns, "Sample5", Some (typeof<obj>), HideObjectMethods = true, IsErased = true)
  do
    typ.DefineStaticParameters(
        parameters,
        fun typeName parameters ->
          let source = string parameters.[0]
          let typ = ProvidedTypeDefinition(asm, ns, typeName, Some typeof<obj>, HideObjectMethods = true, IsErased = true)
          let ctor = ProvidedConstructor(parameters = [ ], InvokeCode= (fun _ -> <@@ source @@>))
          typ.AddMember ctor
          typ.AddMemberDelayed(fun () ->
            let instanceProp = 
              ProvidedProperty(propertyName = "Value", 
                               propertyType = typeof<Hoge>, 
                               GetterCode= (fun _ -> <@@ Fugafuga.f source @@>))
            instanceProp.AddXmlDocDelayed(fun () -> sprintf "<summary><para>%A</para></summary>" (Fugafuga.f source))
            instanceProp)
          typ)
    this.AddNamespace(ns, [typ])

    let thisAssembly = Assembly.GetAssembly(typeof<Sample5ErasedTypeProvider>)
    let path = Path.GetDirectoryName(thisAssembly.Location)
    this.RegisterProbingFolder path

[<assembly:TypeProviderAssembly>] 
do()

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


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

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

  let registerDependencies config registerProbingFolder =
    let thisAssembly = Assembly.GetAssembly(typeof<Style>)
    let path = Path.GetDirectoryName(thisAssembly.Location)
    registerProbingFolder path
      
    let packagePath p = Helper.getUpDirectory 3 path + p
    let currentPath p = path + p
#if NET40
    let tf = "net40"
#endif
#if NET45
    let tf = "net45"
#endif
    let packageConfig = 
      Helper.findConfigFile (config:TypeProviderConfig).ResolutionFolder "packages.config"
    let packageInfo = 
      if File.Exists(packageConfig) then
        use xmlReader = XmlReader.Create(packageConfig)
        let doc = XDocument.Load(xmlReader)
        let (!) x = XName.op_Implicit x
        query {
          for packages in doc.Elements(!"packages") do
          for package in packages.Elements(!"package") do
          select (package.Attribute(!"id").Value, package.Attribute(!"version").Value,package.Attribute(!"targetFramework").Value) } 
      else Seq.empty 

    let getInfo name defaultVersion =  
        match packageInfo |> Seq.tryFind(fun (x,_,_) -> x = name) with
        | Some (_,v,tf) -> v, tf
        | None -> defaultVersion, tf

    let dependencies =
      let core =
        let name = "FsBulletML.Core"
        let version, targetFramework = getInfo name "0.9.0"
        [sprintf @"\%s.%s\lib\%s" name version targetFramework]
      let fparsec = 
        let name = "FParsec"
        let version, _ = getInfo name "1.0.1"
        [sprintf @"\%s.%s\lib\net40-client" name version]
      let parser = 
        let name = "FsBulletML.Parser"
        let version, targetFramework = getInfo name "0.8.6"
        [sprintf @"\%s.%s\lib\%s" name version targetFramework]
      core @ fparsec @ parser

    let packages = 
      dependencies 
      |> Seq.map packagePath
      |> Seq.append (dependencies |> Seq.map currentPath)
      |> Seq.filter (fun x -> Directory.Exists x)
    packages |> Seq.iter registerProbingFolder

ということで、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について、勝手に補足で紹介されていた生成型の型プロバイダーのひじょーにシンプルな例についてご紹介。


まず、消去型のサンプル

namespace Sample.Domain

open System
open System.IO
open System.Linq
open System.Reflection
open Microsoft.FSharp.Core.CompilerServices
open ProviderImplementation.ProvidedTypes

type Hoge =
  | Fuga of string
  | Piyo of int

[<CompilerMessage("hidden...", 13730, IsError = false, IsHidden = true)>]
module Sample = 
  let f source =
    let p,r = Int32.TryParse(source)
    p |> function | true -> Hoge.Piyo r | _ -> Hoge.Fuga source

#nowarn "13730"
[<TypeProvider>] 
type public SampleErasedTypeProvider(cfg:TypeProviderConfig) as this = 
  inherit TypeProviderForNamespaces()
  let asm = Assembly.GetExecutingAssembly()
  let ns = "Sample.Domain"
  let parameters = [ProvidedStaticParameter("source", typeof<string>)]

  let typ = ProvidedTypeDefinition(asm, ns, "Erased", Some (typeof<obj>), HideObjectMethods = true, IsErased = true)
  do
    typ.DefineStaticParameters(
        parameters,
        fun typeName parameters ->
          let source = string parameters.[0]
          let typ = ProvidedTypeDefinition(asm, ns, typeName, Some typeof<obj>, HideObjectMethods = true, IsErased = true)
          let ctor = ProvidedConstructor(parameters = [ ], InvokeCode= (fun _ -> <@@ source @@>))
          typ.AddMember ctor
          typ.AddMemberDelayed(fun () ->
            let instanceProp = 
              ProvidedProperty(propertyName = "Value", 
                               propertyType = typeof<Hoge>, 
                               GetterCode= (fun _ -> <@@ Sample.f source @@>))
            instanceProp.AddXmlDocDelayed(fun () -> sprintf "<summary><para>%A</para></summary>" (Sample.f source))
            instanceProp)
          typ)
    this.AddNamespace(ns, [typ])

[<assembly:TypeProviderAssembly>] 
do()


型が消えてますね。

f:id:zecl:20140825181814p:plain


f:id:zecl:20140825190217p:plain


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

.method public static int32  main(string[] argv) cil managed
{
  .entrypoint
  .custom instance void [FSharp.Core]Microsoft.FSharp.Core.EntryPointAttribute::.ctor() = ( 01 00 00 00 ) 
  // コード サイズ       71 (0x47)
  .maxstack  4
  .locals init ([0] object hoge,
           [1] class [TypeProviderGenType]Sample.Domain.Hoge V_1,
           [2] object V_2,
           [3] class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<class [TypeProviderGenType]Sample.Domain.Hoge,class [FSharp.Core]Microsoft.FSharp.Core.Unit> V_3,
           [4] class [TypeProviderGenType]Sample.Domain.Hoge V_4,
           [5] valuetype [mscorlib]System.ConsoleKeyInfo V_5,
           [6] valuetype [mscorlib]System.ConsoleKeyInfo V_6)
  IL_0000:  nop
  IL_0001:  ldstr      "123"
  IL_0006:  box        [mscorlib]System.String
  IL_000b:  unbox.any  [mscorlib]System.Object
  IL_0010:  stloc.0
  IL_0011:  ldloc.0
  IL_0012:  stloc.2
  IL_0013:  ldstr      "123"
  IL_0018:  call       class [TypeProviderGenType]Sample.Domain.Hoge [TypeProviderGenType]Sample.Domain.Sample::f(string)
  IL_001d:  stloc.1
  IL_001e:  ldstr      "%A"
  IL_0023:  newobj     instance void class [FSharp.Core]Microsoft.FSharp.Core.PrintfFormat`5<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<class [TypeProviderGenType]Sample.Domain.Hoge,class [FSharp.Core]Microsoft.FSharp.Core.Unit>,class [mscorlib]System.IO.TextWriter,class [FSharp.Core]Microsoft.FSharp.Core.Unit,class [FSharp.Core]Microsoft.FSharp.Core.Unit,class [TypeProviderGenType]Sample.Domain.Hoge>::.ctor(string)
  IL_0028:  call       !!0 [FSharp.Core]Microsoft.FSharp.Core.ExtraTopLevelOperators::PrintFormatLine<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<class [TypeProviderGenType]Sample.Domain.Hoge,class [FSharp.Core]Microsoft.FSharp.Core.Unit>>(class [FSharp.Core]Microsoft.FSharp.Core.PrintfFormat`4<!!0,class [mscorlib]System.IO.TextWriter,class [FSharp.Core]Microsoft.FSharp.Core.Unit,class [FSharp.Core]Microsoft.FSharp.Core.Unit>)
  IL_002d:  stloc.3
  IL_002e:  ldloc.1
  IL_002f:  stloc.s    V_4
  IL_0031:  ldloc.3
  IL_0032:  ldloc.s    V_4
  IL_0034:  callvirt   instance !1 class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<class [TypeProviderGenType]Sample.Domain.Hoge,class [FSharp.Core]Microsoft.FSharp.Core.Unit>::Invoke(!0)
  IL_0039:  pop
  IL_003a:  call       valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
  IL_003f:  stloc.s    V_5
  IL_0041:  ldloc.s    V_5
  IL_0043:  stloc.s    V_6
  IL_0045:  ldc.i4.0
  IL_0046:  ret
} // end of method Program::main


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

namespace Sample.Domain

open System
open System.IO
open System.Linq
open System.Reflection
open Microsoft.FSharp.Core.CompilerServices
open ProviderImplementation.ProvidedTypes

type Piyo () = 
  member this.Printfn (v) = printfn "%A" v

#nowarn "13730"
[<TypeProvider>] 
type public SampleNotErasedTypeProvider(cfg:TypeProviderConfig) as this = 
  inherit TypeProviderForNamespaces()
  let asm = Assembly.GetExecutingAssembly()
  let ns = "Sample.Domain"

  let tempAsm = ProvidedAssembly (Path.ChangeExtension (Path.GetTempFileName (), ".dll"))
  do
    let typ = ProvidedTypeDefinition(asm, ns, "NotErased", Some (typeof<obj>), IsErased = false)
    tempAsm.AddTypes [typ]
    let parameters = [ProvidedStaticParameter("source", typeof<string>)]
    typ.DefineStaticParameters (parameters, this.GenerateTypes)
    this.AddNamespace(ns, [typ])

  member internal this.GenerateTypes (typeName: string) (args: obj[]) =
    let source = string args.[0]
    let typ = ProvidedTypeDefinition (asm, ns, typeName, Some typeof<Piyo>, IsErased = false)
    let ctor = ProvidedConstructor(parameters = [ ], InvokeCode= (fun _ -> <@@ source @@>))
    typ.AddMember ctor
    typ.AddMemberDelayed(fun () ->
      let instanceProp = 
        ProvidedProperty(propertyName = "Value", 
                         propertyType = typeof<Hoge>, 
                         GetterCode= (fun _ -> <@@ Sample.f source @@>))
      instanceProp.AddXmlDocDelayed(fun () -> sprintf "<summary><para>%A</para></summary>" (Sample.f source))
      instanceProp)
    tempAsm.AddTypes [typ]
    typ

[<assembly:TypeProviderAssembly>] 
do()

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

f:id:zecl:20140825181847p:plain


f:id:zecl:20140825181909p:plain


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

.method public static int32  main(string[] argv) cil managed
{
  .entrypoint
  .custom instance void [FSharp.Core]Microsoft.FSharp.Core.EntryPointAttribute::.ctor() = ( 01 00 00 00 ) 
  // コード サイズ       53 (0x35)
  .maxstack  4
  .locals init ([0] class Program/HogeA hoge,
           [1] class [TypeProviderGenType]Sample.Domain.Hoge V_1,
           [2] class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<class [TypeProviderGenType]Sample.Domain.Hoge,class [FSharp.Core]Microsoft.FSharp.Core.Unit> V_2,
           [3] class [TypeProviderGenType]Sample.Domain.Hoge V_3,
           [4] valuetype [mscorlib]System.ConsoleKeyInfo V_4,
           [5] valuetype [mscorlib]System.ConsoleKeyInfo V_5)
  IL_0000:  nop
  IL_0001:  newobj     instance void Program/HogeA::.ctor()
  IL_0006:  stloc.0
  IL_0007:  ldloc.0
  IL_0008:  callvirt   instance class [TypeProviderGenType]Sample.Domain.Hoge Program/HogeA::get_Value()
  IL_000d:  stloc.1
  IL_000e:  ldstr      "%A"
  IL_0013:  newobj     instance void class [FSharp.Core]Microsoft.FSharp.Core.PrintfFormat`5<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<class [TypeProviderGenType]Sample.Domain.Hoge,class [FSharp.Core]Microsoft.FSharp.Core.Unit>,class [mscorlib]System.IO.TextWriter,class [FSharp.Core]Microsoft.FSharp.Core.Unit,class [FSharp.Core]Microsoft.FSharp.Core.Unit,class [TypeProviderGenType]Sample.Domain.Hoge>::.ctor(string)
  IL_0018:  call       !!0 [FSharp.Core]Microsoft.FSharp.Core.ExtraTopLevelOperators::PrintFormatLine<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<class [TypeProviderGenType]Sample.Domain.Hoge,class [FSharp.Core]Microsoft.FSharp.Core.Unit>>(class [FSharp.Core]Microsoft.FSharp.Core.PrintfFormat`4<!!0,class [mscorlib]System.IO.TextWriter,class [FSharp.Core]Microsoft.FSharp.Core.Unit,class [FSharp.Core]Microsoft.FSharp.Core.Unit>)
  IL_001d:  stloc.2
  IL_001e:  ldloc.1
  IL_001f:  stloc.3
  IL_0020:  ldloc.2
  IL_0021:  ldloc.3
  IL_0022:  callvirt   instance !1 class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<class [TypeProviderGenType]Sample.Domain.Hoge,class [FSharp.Core]Microsoft.FSharp.Core.Unit>::Invoke(!0)
  IL_0027:  pop
  IL_0028:  call       valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
  IL_002d:  stloc.s    V_4
  IL_002f:  ldloc.s    V_4
  IL_0031:  stloc.s    V_5
  IL_0033:  ldc.i4.0
  IL_0034:  ret
} // end of method Program::main


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




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

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

型プロバイダー(Type Provider)のとりとめもない話

だらだらと型プロバイダー(Type Provider)のとりとめもない話。ほとんど内容はないよう。 

   

型プロバイダーって何?

F#3.0 から利用できる目玉機能のひとつ。 これが発表されたとき、「LINQ + Type Providers = Information Rich Programming」なんて言われていました。 「インフォメーションリッチプログラミングから LINQ を引いて残った物が 型プロバイダーだよ。」「わけがわからないよ。」

  任意のメタデータを与えると、コンパイラコンパイル時に"型のないもの"に"型を与えて"提供してくれる。それが型プロバイダー。これ、Visual Studio や Xamarin(MonoDevelop) 等のIDEとも連携するということでメシウマ状態必至なわけ。  

type Person = JsonProvider<""" { "name":"Jiraiya", "age":54 } """>
let person = Person.Parse(""" { "name":"Naruto", "age":16 } """)
person.Name // わーおw(゚o゚)wインテリセンスが効くし型も付いている
person.Age  // わーおw(゚o゚)wインテリセンスが効くし型も付いている

 

型プロバイダーが型として提供できないようなメタデータが型プロバイダーに与えられた場合、コンパイルエラーとなる(そのように実装している場合に限る)。型プロバイダーの主目的はコンパイル時プログラミングではないものの、型のないものを扱う際に生じやすいSHOUMONAIバグを未然に防ぐことができるソリューションと言うこともできる。型のないものを安全に扱える世界があなたの手中に的な。

     
型プロバイダーさんステキ!抱いて! 

 

   

型プロバイダーの作成って難しそう(実際難しい)

一応それなりに F# を追っかけているマンなので、某サンプルの「Hello World TypeProvider」くらいはチョロっとかじりました。

  チュートリアル : 型プロバイダーの作成 (F#) 
 

でも、特に作りたいオリジナルのネタもなくって(あるいは既に先駆者がいたりで)、なかなか触れる機会に恵まれまれずにコンニチに至ります(言い訳)。そういう人多そう。実装したいネタがどうこうというより、そもそも難しかったり取っつきにくかったりで手が出しずらいという説もある。もし詳細を知りたいなら FSharp.Data等のライブラリのソース嫁がデフォというなかなかに漢らしい感じだし。何か作りながら覚えようにも、予期せぬエラーとそのメッセージに悩まされることも多いし、デバッグ実行で地道に追っても原因がよくわからず途方に暮れるなんてこともあり...結構ハードルは高い。実際上級者向けではある(個人の感想)。 

 

「型プロバイダーってアレでしょ?作るもんじゃなくて、使うもんでしょ?」 

 

難しいことはよくわからないけど、F# のすごい人たちが「"型のないもの"に"型を付けてくれる"」とっても便利でクールなライブラリを作ってくれていて、「オレのような一般 F# ユーザーは利用者としてその恩恵を受けることができればいいよね。」くらいに割り切って押さえておくだけでもよいという代物かもしれない。

 

   

ごもっともすぎるご意見。汎用的で有用な型プロバイダーを自分で実装するような機会は、あまりないような気がする(便利そうなやつはもう既に誰かが作ってくれていることが多いし)。概要だけ把握しておいて、もし実装する機会があったらそのときじっくり調べながら頑張ればいいんじゃないだろうか。必要に応じて。 

 

    
   【朗報】型プロバイダーは華麗にスルーしても上級者になれる(!) 逆に言うと、型プロバイダーがわかったからって上級者にはなれません。


型プロバイダーの作成って面白そう

2014年8月3日に F# Meetup in TokyoおよびそのPart1という素晴らしいイベントがあり、fsugjp がやまだかつてない程の盛り上がりがあったようです。そのイベントで@kos59125さんによるTypeProviderに関する発表があり、更に後日@bleisさんによる、TypeProviderについて、勝手に補足がありました。素晴らしい。私は参加できませんでしたが、F# Meetup in Tokyo #fsugjp - Togetterまとめで雰囲気だけ味わいました。 触発されて何かしら型プロバイダーを作りたくなった。軽い気持ちで...、とりあえず役に立たない系型プロバイダーを作ってみた。  

    @moonmileさんにフィードバックを頂きました。どうもありがとうございます。 

 

 
ゼロから自分で実装することも不可能ではありませんが...基本的にはこの方法しかないといっても過言ではないので、なにかしらのProvidedType.fs(の類)を使いましょう。 役に立つ立たないは置いておいて勉強したい場合はとりあえずなんか作ってみる。そうじゃやない場合は概要だけ抑えて華麗にスルーする。でいいと思います。 



f:id:zecl:20140825173101p:plain



printf系の "%A" 書式指定子における型の表示レイアウトのカスタマイズ

判別共用体を文字列として出力する際に、ケース識別子を宣言する型(判別共用体)の名前を含めたフルネームで文字列化したくなったときのお話。

たとえば、以下を実行すると

type Tree<'T> =
  | Leaf of 'T
  | Node of Tree<'T> * Tree<'T>

let tree1 = Node(Node(Leaf("a"),Node(Leaf("b"),Node(Leaf("c"),Leaf("d")))),Node(Leaf("e"),Leaf("f")))
printfn "%A" tree1

次の出力結果を得られる。

Node
  (Node (Leaf "a",Node (Leaf "b",Node (Leaf "c",Leaf "d"))),
   Node (Leaf "e",Leaf "f"))


それを、以下のような感じに出力するようにしたい。というのが今回のお題。

Tree.Node
  (Tree.Node (Tree.Leaf "a",Tree.Node (Tree.Leaf "b",Tree.Node (Tree.Leaf "c",Tree.Leaf "d"))),
   Tree.Node (Tree.Leaf "e",Tree.Leaf "f"))


StructuredFormatDisplay属性を使う

Core.StructuredFormatDisplayAttribute クラス (F#) - MSDNライブラリ

この属性は、%A printf 書式設定やその他の 2 次元のテキストベースの表示レイアウトを使用する場合に、型を表示する既定の方法を指定するために使用されます。 このバージョンの F# で有効な値は、PreText {PropertyName} PostText 形式の値のみです。 プロパティ名は、オブジェクトそのものの代わりに評価および表示するプロパティを表します。

とある。これを使えば、型を表示する際のレイアウトを自由にカスタマイズすることができる。なお、"このバージョンの F# で有効な値は"とあるが、F#2.0からF#3.1までは変更はない(F#2.0より前は仕様が異なる)。


StructuredFormatDisplayAttribute。自作ライブラリをせっせとこさえていたり、某有名F# ライブラリのソースコード等を読んでたり、コンパイラのソースを見ていたりする人なら見覚えがあるかもしれないが、"%A"書式指定子の表示レイアウトをカスタマイズしたい場面はそんなに多くはないだろうし、そこそこマニアックな(割とどーでもいい)話題かもしれない。こちら、「実践F#」に載っていないというか、「Expert F#3.0」にも載ってなかったと思うし、いまのところ最新の言語仕様書であるところの「The F# 3.0 Language Specification」にも記載されていないようなので、言語仕様書熟読勢も把握できていない可能性がある。でも、「プログラミングF#」にはサラりと載っていたりする(!)。


単純な例

StructuredFormatDisplay属性を使った単純な例は以下のようになる(ここでは例として判別共用体を対象としているが、その限りではない)。

[<StructuredFormatDisplay("Hello{Display}!")>]
type Hello = 
  | Hello of string 
  member private this.Display = 
    match this with 
    | Hello s -> sprintf ", %s" s

let hello = Hello("F#")
printfn "%A" hello

出力結果は以下のようになる。

Hello, F#!

使い方めちゃ簡単。

FizzBuzzしてみる

意味もなくFizzBuzzしてみます。

[<StructuredFormatDisplay("{Display}")>]
type FizzBuzz = 
  | FizzBuzz of Fizz * Buzz
  member private this.Display =
      let (|Mul|_|) x y = if y % x = 0 then Some(y / x) else None
      let fizzbuzz x y = 
        let xy = x * y
        [1..100] |> List.map (function
        | Mul  xy _ -> "FizzBuzz"
        | Mul  x _ -> "Fizz"
        | Mul  y _ -> "Buzz"
        | n -> string n)

      match this with
      | FizzBuzz (Fizz(x), Buzz(y)) -> fizzbuzz x y

and Fizz = Fizz of int
and Buzz = Buzz of int

let fizzbuzz = FizzBuzz(Fizz(3),Buzz(5))
printfn "%A" fizzbuzz
printfn "%s" <| fizzbuzz.ToString()
printfn "%O" fizzbuzz

出力結果

["1"; "2"; "Fizz"; "4"; "Buzz"; "Fizz"; "7"; "8"; "Fizz"; "Buzz"; "11"; "Fizz";
 "13"; "14"; "FizzBuzz"; "16"; "17"; "Fizz"; "19"; "Buzz"; "Fizz"; "22"; "23";
 "Fizz"; "Buzz"; "26"; "Fizz"; "28"; "29"; "FizzBuzz"; "31"; "32"; "Fizz"; "34";
 "Buzz"; "Fizz"; "37"; "38"; "Fizz"; "Buzz"; "41"; "Fizz"; "43"; "44";
 "FizzBuzz"; "46"; "47"; "Fizz"; "49"; "Buzz"; "Fizz"; "52"; "53"; "Fizz";
 "Buzz"; "56"; "Fizz"; "58"; "59"; "FizzBuzz"; "61"; "62"; "Fizz"; "64"; "Buzz";
 "Fizz"; "67"; "68"; "Fizz"; "Buzz"; "71"; "Fizz"; "73"; "74"; "FizzBuzz"; "76";
 "77"; "Fizz"; "79"; "Buzz"; "Fizz"; "82"; "83"; "Fizz"; "Buzz"; "86"; "Fizz";
 "88"; "89"; "FizzBuzz"; "91"; "92"; "Fizz"; "94"; "Buzz"; "Fizz"; "97"; "98";
 "Fizz"; "Buzz"]
Program+FizzBuzz
Program+FizzBuzz


この結果から、StructuredFormatDisplay属性を使って型の表示方法をカスタマイズしても、"%s"および"%O"書式指定子に影響を及ぼしていないことが確認できる。"%s"および"%O"書式指定子を指定した場合、いずれも結果的に対象オブジェクトについて Object.ToString仮想メソッドが呼び出されるかたちになるので、判別共用体の場合は既定では上記のように型名が出力される。 override this.ToString () = sprintf "%A" this.Displayというように、ToStringメソッドをオーバーライドする実装を追加すれば、いずれも "%A"書式指定子を指定した場合と同じ結果が得られるようになる。F#2.0より前のバージョンでは ToStringを経由して表示する際にStructuredFormatDisplay属性を参照していたようだが、F#2.0以降はToStringメソッドを経由する場合にはこれを参照しないよう仕様が変更された。

StructuredFormatDisplay属性で指定した{PropertyName}を実装していない場合

ちょっと例を変えて、レコード型にしてみる。

[<StructuredFormatDisplay("{AsString}")>]
type myRecord = 
  {value : int}
  override this.ToString() = "hello"
  //member this.AsString = this.ToString()

let t = {value=5}
printfn "%s" (t.ToString())
printfn "%O" t
printfn "%A" t

出力結果

hello
hello
<StructuredFormatDisplay exception: メソッド 'Program+myRecord.AsString' が見つかりません。>

とまあ、StructuredFormatDisplay属性で指定した{PropertyName}を実装していない場合は、 コンパイルエラーとなるわけでなく例外となるわけでなく、割と残念な感じの出力結果を得ることになる。コンパイルエラーにしてくれてもいいのにー。

判別共用体(discriminated unions)について、型名も含めて文字列化する

さて、本題。

まずは愚直に書いてみよう

StructuredFormatDisplay属性でマークし、表示をカスタマイズする実装を愚直に書き加える。

[<StructuredFormatDisplay("{Display}")>]
type Tree<'T> =
  | Leaf of 'T
  | Node of Tree<'T> * Tree<'T>
  member private t.Display = 
    match t with
    | Leaf x -> sprintf "%s %A" "Tree.Leaf" x 
    | Node (a,b) -> sprintf "%s %A" "Tree.Node" (a,b) 

let tree1 = Node(Node(Leaf("a"),Node(Leaf("b"),Node(Leaf("c"),Leaf("d")))),Node(Leaf("e"),Leaf("f")))
printfn "%A" tree1

以下の出力結果が得られる。

Tree.Node (Tree.Node (Tree.Leaf "a",
 Tree.Node (Tree.Leaf "b", Tree.Node (Tree.Leaf "c", Tree.Leaf "d"))),
 Tree.Node (Tree.Leaf "e", Tree.Leaf "f"))

おいおい。PreText使おうぜ

あっ。ケース識別子を宣言する型(判別共用体)の名前は固定なので、この場合StructuredFormatDisplay属性のPreTextに集約できるんだったね。

[<StructuredFormatDisplay("Tree.{Display}")>]
type Tree<'T> =
  | Leaf of 'T
  | Node of Tree<'T> * Tree<'T>
  member private t.Display = 
    match t with
    | Leaf x -> sprintf "%s %A" "Leaf" x 
    | Node (a,b) -> sprintf "%s %A" "Node" (a,b) 

let tree1 = Node(Node(Leaf("a"),Node(Leaf("b"),Node(Leaf("c"),Leaf("d")))),Node(Leaf("e"),Leaf("f")))
printfn "%A" tree1

出力結果変わらず。

Tree.Node (Tree.Node (Tree.Leaf "a",
 Tree.Node (Tree.Leaf "b", Tree.Node (Tree.Leaf "c", Tree.Leaf "d"))),
 Tree.Node (Tree.Leaf "e", Tree.Leaf "f"))

このTree<'T>判別共用体の場合だけに関して言えば、とりあえずこれで良さそうに見えるし、この方法を取れば他の判別共用体についても都度対応できそうだ。 でも、毎回個別に対応するなんてダルすぎる。汎用的にしたいよねー。

リフレクションで汎用的に実装しよう

Microsoft.FSharp.Reflectionを利用する。

open Microsoft.FSharp.Reflection 

let stringifyFullName (discriminatedUnion:'T) = 
  if box discriminatedUnion = null  then
    nullArg  "discriminatedUnion"   
  if FSharpType.IsUnion(typeof<'T>)|> not then
    invalidArg "discriminatedUnion" (sprintf "判別共用体じゃないよ:%s" typeof<'T>.FullName)

  let info, objects = FSharpValue.GetUnionFields(discriminatedUnion, typeof<'T>)
  let typeName = 
    if info.DeclaringType.IsGenericType then
      info.DeclaringType.Name.Substring(0, info.DeclaringType.Name.LastIndexOf("`"))  + "." + info.Name
    else
      info.DeclaringType.Name + "." + info.Name
  match objects  with
  | [||] -> typeName
  | elements -> 
    let fields = info.GetFields()
    if fields.Length = 1 then
      sprintf "%s %A" typeName elements.[0]
    else
      let tupleType = 
        fields
        |> Array.map( fun pi -> pi.PropertyType )
        |> FSharpType.MakeTupleType
      let tuple = FSharpValue.MakeTuple(elements, tupleType)
      sprintf "%s %A" typeName tuple

[<StructuredFormatDisplay("{ToStructuredDisplay}")>]
type Tree<'T> =
  | Leaf of 'T
  | Node of Tree<'T> * Tree<'T>
  member private t.ToStructuredDisplay = t.ToString()
  override t.ToString () = stringifyFullName t 

let tree1 = Node(Node(Leaf("a"),Node(Leaf("b"),Node(Leaf("c"),Leaf("d")))),Node(Leaf("e"),Leaf("f")))
printfn "%A" tree1

出力結果

Tree.Node (Tree.Node (Tree.Leaf "a",
 Tree.Node (Tree.Leaf "b", Tree.Node (Tree.Leaf "c", Tree.Leaf "d"))),
 Tree.Node (Tree.Leaf "e", Tree.Leaf "f"))

ヽ(*´∀`)ノ ワーイ、できたよー

と、思ったけど、待って。違うやん。本当は以下のようなレイアウトで表示したかったんだった(だった!)。

Tree.Node
  (Tree.Node (Tree.Leaf "a",Tree.Node (Tree.Leaf "b",Tree.Node (Tree.Leaf "c",Tree.Leaf "d"))),
   Tree.Node (Tree.Leaf "e",Tree.Leaf "f"))


んー、内容的には同じなのでそんなに大きな問題ではないんだけど、若干モヤッとする。 "%A" 書式指定子の表示レイアウトをいい感じに制御するにはどうすればよいのだろう? また、既存の型(例えばOption<'T>型など)の、表示をカスタマイズしたい場合はどうすればよいのだろう?

F#er諸兄、何かご存じであればアドバイス頂きたい。

判別共用体で型付きDSL。弾幕記述言語BulletMLのF#実装、FsBulletML作りました。

この記事はF# Advent Calendar 201320日目です。



遅ればせながらThe Last of Usをちびちびとプレイ中。FF14のパッチ2.1が先日リリースされ、メインジョブ弱体化にもめげず引き続き光の戦士としてエオルゼアの平和を守り続けている今日この頃。艦これは日課です。はてなブログに引っ越してきて一発目(ブログデザイン模索中)です。気付けば半年もブログを書いていませんでした(テイタラク)。今年はあまり.NET関係の仕事に携わることができずに悶々としておりましたが、急遽C#の仕事が舞い込んできました。年末はいろいろとバタバタするものです。少しの間ホテル暮らしなのでゲーム(据置機)できなくてつらいです。わたしは大晦日にひとつの節目を迎えます。記憶力、体力、集中力...多くのものを失ったように思います。アラフォーいい響きです(白目)。


上の動画の弾幕は次のBulletml判別共用体で記述されています(MonoGameで動いています)。

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
27: 
28: 
29: 
30: 
31: 
32: 
33: 
34: 
35: 
#r @"bin\Debug\FsBulletML.Core.dll"
open FsBulletML

/// ぐわんげ、二面ボス by 白い弾幕くん
/// [Guwange]_round_2_boss_circle_fire.xml
let ``round_2_boss_circle_fire`` = 
  "ぐわんげ二面ボス by 白い弾幕くん",
  Bulletml
    ({bulletmlXmlns = Some "http://www.asahi-net.or.jp/~cs8k-cyu/bulletml";
      bulletmlType = Some BulletVertical;},
      [BulletmlElm.Fire
        ({fireLabel = Some "circle";},
          Some (Direction (Some {directionType = DirectionType.Sequence;},"$1")),
          Some (Speed (None,"6")),
          Bullet
            ({bulletLabel = None;},None,None,
            [Action
                ({actionLabel = None;},
                [Wait "3";
                  Fire
                    ({fireLabel = None;},
                    Some (Direction (Some {directionType = DirectionType.Absolute;},"$2")),
                    Some (Speed (None,"1.5+$rank")),
                    Bullet ({bulletLabel = None;},None,None,[])); Vanish])]));
      BulletmlElm.Action
        ({actionLabel = Some "fireCircle";},
          [Repeat
            (Times "18",
              Action
                ({actionLabel = None;},
                [FireRef ({fireRefLabel = "circle";},["20"; "$1"])]))]);
      BulletmlElm.Action
        ({actionLabel = Some "top";},
          [Action.ActionRef ({actionRefLabel = "fireCircle";},["180-45+90*$rand"]);
          Wait "10"])])
名前空間 FsBulletML
Multiple items
共用体ケース Bulletml.Bulletml: BulletmlAttrs * BulletmlElm list -> Bulletml

--------------------
type Bulletml =
  | Bulletml of BulletmlAttrs * BulletmlElm list
  | Action of ActionAttrs * Action list
  | ActionRef of ActionRefAttrs * Params
  | Fire of FireAttrs * Direction option * Speed option * BulletElm
  | FireRef of FireRefAttrs * Params
  | Wait of string
  | Vanish
  | ChangeSpeed of Speed * Term
  | ChangeDirection of Direction * Term
  | Accel of Horizontal option * Vertical option * Term
  ...

完全名: FsBulletML.DTD.Bulletml
共用体ケース Option.Some: 'T -> Option<'T>
共用体ケース ShootingDirection.BulletVertical: ShootingDirection
type BulletmlElm =
  | Bullet of BulletAttrs * Direction option * Speed option * ActionElm list
  | Fire of FireAttrs * Direction option * Speed option * BulletElm
  | Action of ActionAttrs * Action list

完全名: FsBulletML.DTD.BulletmlElm
共用体ケース BulletmlElm.Fire: FireAttrs * Direction option * Speed option * BulletElm -> BulletmlElm
Multiple items
共用体ケース Direction.Direction: DirectionAttrs option * string -> Direction

--------------------
type Direction = | Direction of DirectionAttrs option * string

完全名: FsBulletML.DTD.Direction
type DirectionType =
  | Aim
  | Absolute
  | Relative
  | Sequence

完全名: FsBulletML.DTD.DirectionType
共用体ケース DirectionType.Sequence: DirectionType
Multiple items
共用体ケース Speed.Speed: SpeedAttrs option * string -> Speed

--------------------
type Speed = | Speed of SpeedAttrs option * string

完全名: FsBulletML.DTD.Speed
共用体ケース Option.None: Option<'T>
共用体ケース BulletElm.Bullet: BulletAttrs * Direction option * Speed option * ActionElm list -> BulletElm
Multiple items
共用体ケース ActionElm.Action: ActionAttrs * Action list -> ActionElm

--------------------
type Action =
  | ChangeDirection of Direction * Term
  | Accel of Horizontal option * Vertical option * Term
  | Vanish
  | ChangeSpeed of Speed * Term
  | Repeat of Times * ActionElm
  | Wait of string
  | Fire of FireAttrs * Direction option * Speed option * BulletElm
  | FireRef of FireRefAttrs * Params
  | Action of ActionAttrs * Action list
  | ActionRef of ActionRefAttrs * Params

完全名: FsBulletML.DTD.Action
共用体ケース Action.Wait: string -> Action
共用体ケース Action.Fire: FireAttrs * Direction option * Speed option * BulletElm -> Action
共用体ケース DirectionType.Absolute: DirectionType
共用体ケース Action.Vanish: Action
共用体ケース BulletmlElm.Action: ActionAttrs * Action list -> BulletmlElm
共用体ケース Action.Repeat: Times * ActionElm -> Action
Multiple items
共用体ケース Times.Times: string -> Times

--------------------
type Times = | Times of string

完全名: FsBulletML.DTD.Times
共用体ケース Action.FireRef: FireRefAttrs * Params -> Action
共用体ケース Action.ActionRef: ActionRefAttrs * Params -> Action

FsBulletMLリリースしました

弾幕記述言語BulletMLF#実装、FsBulletMLを作りました。Unity 4.3では新たに2Dがサポートされたりで、少なからず需要がないこともないのかもしれず。せっかくなので FsBulletML.Core(内部DSLを提供) および、FsBulletML.Parser(外部DSLを提供) をNuGetに放流してみました(Beta版)。実際に使える(使われる)ライブラリに成長するかどうかはわかりません。

詳しくはこちらをご覧ください

BulletMLとは

BulletMLとは、シューティングゲームにおける弾幕を記述するための言語(外部DSL)で、多くのハイクオリティなシューティングゲームを開発なさっている ABA Games の 長健太氏が作りました。BulletMLが初めて公開されたのは2002年頃でしょうか? もう10年以上前ということになります。シンプルな記述で多彩な弾幕を表現することができ、有限オートマトン的に弾を管理しなくてもよいので楽チン。ということで多くの人から注目を集めました。わたしが存在を知ったのはもう少し後のことですが、当時かなりインパクトを受けて感動したのを覚えています。

本家BulletMLのパーサおよび処理系はJavaで実装されています。RELAX定義DTD定義など弾幕定義自体の簡易的な仕様については公式に公開されているものの、弾幕の処理系の詳細については「ソース嫁」状態という非常に漢気溢れる感じになっています*1。にもかかわらず、多くの人によって様々な言語で移植/実装/改良されていますBulletSML は、BulletMLのS式版(内部DSL)で、ひとつひとつの弾が継続になっているらしいです(あたまおかしい)。最近では、bulletml.js が内部DSLも提供していて、enchant.js用、tmlib.js用のプラグインもあって、ブラウザで動く弾幕ゲーが簡単に作れるようになっているようです。

特定の「何か」を達成するために、最適化された言語を作ることは簡単なことではありません。シンプルで且つ表現力が高くて実用的なドメイン固有言語を作る(デザインする)のはとても難しいことです。BulletMLはとてもよくデザインされていて面白くて魅力的なDSLだと思いました。

DSLについて

以前以下のような記事を書きました。

F#3.0で加速する言語指向プログラミング(LOP)。コンピューテーション式はもはやモナドだけのための構文ではない!!!

きっかけ

今年は例年に比べてIT系勉強会に参加できませんでしたが、夏に「コード書こうぜ!」をスローガンとしたCode2013という合宿イベントに参加しました。その中で、座談会orセミナー形式で複数グループが集まってそれぞれが異なるテーマについて話をする形の「きんぎょばち」というコーナーがありまして、「パーサコンビネータを使った構文解析およびDSLの作成などについて勉強したいです。」というテーマがありました。特定言語に限ったテーマではありませんが、他に「F# や 関数型言語の話題なら少し話せます。」というテーマもあったので、わたしが F# について話をする流れになって、判別共用体の便利さや FParsec を用いた字句解析、構文解析等についてお話してきました。本当は教えてもらいたい側だったのですが...。イベントから帰ってきてから、「何かDSL書きたいなー」と漠然とした思いを持っていました。

BulletMLを 判別共用体で 型付きの内部DSLとして表現できるようにしたらちょっと面白いんじゃあ?」という発想。アイディアとしては3年くらい前から持っていましたが、実装が面倒くさいことになりそうだったので行動に至らずでした。ゲーム開発の世界ではDSLや各ドメインエキスパートのための独自スクリプト言語などを開発/運用することは日常茶飯事で、そう珍しいことではないと風のうわさで聞いたことがあります。わたし自身は、仕事であれ趣味であれ、日頃のソフトウェア開発において本格的なDSLを設計したり実装したりする機会はほとんどありません。非常に興味のある分野/開発手法なので実際に何かを作って勉強してみたい。そう兼ねてから思っていました。良い練習になりそうだし、"評論家先生"というYAIBAに影響を受けたりで、重い腰を上げました(よっこらせ)。

やりたかったこと

弾幕を判別共用体で書きたい(型付き内部DSL)
・従来のXML形式での弾幕を再利用したい(外部DSL)
XML形式は書きにくいしちょっと古臭い。XMLとは異なる形式の外部DSLも利用できるようにしたい。

要は、「内部DSLと複数の外部DSLを両立する。」ということをやってみたい。その辺りを最終的な目標にしました。そもそもC#での実装(BulletML C# - Bandle Games)があるし、何を今さら感があるのも事実ですが、判別共用体による型付き内部DSLを提供するという点で若干のアドバンテージがあります。型付きの内部DSLが使えると何がうれしいって、弾幕を構築する際にF#の式がそのまま使えるということです。つまり、関数を組み合わせて自由に弾幕を組み立てることができるようになります。それってつまり、Bulletsmorphのようなアイディアも実装しやすくなる、と。

XMLのdisり大会。こちら結局どうなったんでしょう。気になります(´・_・`)

DTD定義を判別共用体で表現する

いにしえからの言い伝えによると、「判別共用体は、ドメイン知識をモデル化するための簡潔かつタイプセーフな方法である。」らしいです。 ということで、BulletMLDTD定義を内部DSLとして判別共用体で表現する。ということについて考えてみたい。

以前、こんなやり取りがありました。



言語内に型付きDSLを構築したいようなケースでは、GADT(Generalised Algebraic Datatypes)が欲しくなるようです。つまりこれ、抽象構文木なんかを型付きで表したいときに発生する事案です。しかし、F#にはHaskellGADTsに相当するものはありません。GADT相当の表現自体はOOPスタイルで書けば可能ではありますが、判別共用体で内部DSLを表現したいというコンセプトとはズレてしまうので今回は適用できません。仕方がないので、型を細分化してどんどん型が絞られていくような型を定義します。

BulletMLのDTD定義に沿って、以下のような感じの型を定義すれば、判別共用体による内部DSLの構造(モロ抽象構文木)が表現できます。

  /// BulletML DTD
  /// <!ELEMENT vertical (#PCDATA)>
  /// <!ATTLIST vertical type (absolute|relative|sequence) "absolute">
  type Vertical =
  | Vertical of VerticalAttrs option * string 
  and VerticalAttrs = { verticalType : VerticalType }
  and VerticalType = 
  | Absolute 
  | Relative
  | Sequence

  /// BulletML DTD
  /// <!ELEMENT param (#PCDATA)>  
  type Params = string list

  /// BulletML DTD
  /// <!ELEMENT speed (#PCDATA)>
  /// <!ATTLIST speed type (absolute|relative|sequence) "absolute">
  type Speed =
  | Speed of SpeedAttrs option * string
  and SpeedAttrs = { speedType : SpeedType }
  and SpeedType = 
  | Absolute 
  | Relative 
  | Sequence

  /// BulletML DTD
  /// <!ELEMENT direction (#PCDATA)>
  /// <!ATTLIST direction type (aim|absolute|relative|sequence) "aim">
  type Direction = 
  | Direction of DirectionAttrs option * string
  and DirectionAttrs = { directionType : DirectionType }
  and DirectionType =
  | Aim | Absolute | Relative | Sequence

  /// BulletML DTD
  /// <!ELEMENT term (#PCDATA)>
  type Term = Term of string

  /// BulletML DTD
  /// <!ELEMENT times (#PCDATA)>
  type Times = Times of string

  /// BulletML DTD
  /// <!ELEMENT horizontal (#PCDATA)>
  /// <!ATTLIST horizontal type (absolute|relative|sequence) "absolute">
  type Horizontal = 
  | Horizontal of HorizontalAttrs option * string
  and HorizontalAttrs = { horizontalType : HorizontalType }
  and HorizontalType = 
  | Absolute // Default
  | Relative
  | Sequence

  type BulletmlAttrs = { bulletmlXmlns : string option; bulletmlType : ShootingDirection option}
  and ShootingDirection = 
  | BulletNone // Default 
  | BulletVertical 
  | BulletHorizontal
  type ActionAttrs = { actionLabel : string option }
  type ActionRefAttrs = { actionRefLabel : string }
  type FireAttrs = { fireLabel : string option }
  type FireRefAttrs = { fireRefLabel : string }
  type BulletAttrs = { bulletLabel : string option }
  type BulletRefAttrs = { bulletRefLabel : string }

  type Bulletml =
/// BulletML DTD
/// <!ELEMENT bulletml (bullet | fire | action)*>
/// <!ATTLIST bulletml xmlns CDATA #IMPLIED>
/// <!ATTLIST bulletml type (none|vertical|horizontal) "none">
  | Bulletml of BulletmlAttrs * BulletmlElm list 
/// BulletML DTD
/// <!ELEMENT action (changeDirection | accel | vanish | changeSpeed | repeat | wait | (fire | fireRef) | (action | actionRef))*>
/// <!ATTLIST action label CDATA #IMPLIED>
  | Action of ActionAttrs * Action list 
/// BulletML DTD
/// <!ELEMENT actionRef (param* )>
/// <!ATTLIST actionRef label CDATA #REQUIRED>
  | ActionRef of ActionRefAttrs * Params
/// BulletML DTD
/// <!ELEMENT fire (direction?, speed?, (bullet | bulletRef))>
/// <!ATTLIST fire label CDATA #IMPLIED>
  | Fire of FireAttrs * Direction option * Speed option * BulletElm  
/// BulletML DTD
/// <!ELEMENT fireRef (param* )>
/// <!ATTLIST fireRef label CDATA #REQUIRED>
  | FireRef of FireRefAttrs * Params
/// BulletML DTD
/// <!ELEMENT wait (#PCDATA)>
  | Wait of string
/// BulletML DTD
/// <!ELEMENT vanish (#PCDATA)>
  | Vanish 
/// BulletML DTD
/// <!ELEMENT changeSpeed (speed, term)>
  | ChangeSpeed of Speed * Term
/// BulletML DTD
/// <!ELEMENT changeDirection (direction, term)>
  | ChangeDirection of Direction * Term
/// BulletML DTD
/// <!ELEMENT accel (horizontal?, vertical?, term)>  
  | Accel of Horizontal option * Vertical option * Term
/// BulletML DTD
/// <!ELEMENT bullet (direction?, speed?, (action | actionRef)* )>
/// <!ATTLIST bullet label CDATA #IMPLIED>
  | Bullet of BulletAttrs * Direction option * Speed option * ActionElm list 
/// BulletML DTD
/// <!ELEMENT bulletRef (param* )>
/// <!ATTLIST bulletRef label CDATA #REQUIRED>
  | BulletRef of BulletRefAttrs * Params
/// BulletML DTD
/// <!ELEMENT repeat (times, (action | actionRef))>
  | Repeat of Times * ActionElm 
  | NotCommand

  and BulletmlElm =
  | Bullet of BulletAttrs * Direction option * Speed option * ActionElm list 
  | Fire of FireAttrs * Direction option * Speed option * BulletElm 
  | Action of ActionAttrs * Action list 

  and Action = 
  | ChangeDirection of Direction * Term
  | Accel of Horizontal option * Vertical option * Term
  | Vanish 
  | ChangeSpeed of Speed * Term
  | Repeat of Times * ActionElm 
  | Wait of string
  | Fire of FireAttrs * Direction option * Speed option * BulletElm 
  | FireRef of FireRefAttrs * Params
  | Action of ActionAttrs * Action list 
  | ActionRef of ActionRefAttrs * Params

  and BulletElm =
  | Bullet of BulletAttrs * Direction option * Speed option * ActionElm list 
  | BulletRef of BulletRefAttrs * Params

  and ActionElm =
  | Action of ActionAttrs * Action list 
  | ActionRef of ActionRefAttrs * Params

長い。ここで定義したBulletml判別共用体は、確かに型付きDSLではあるのですが、BulletMLの仕様に準拠するかたちで型を表現するようにしたのでタイプセーフではないですね。タイプセーフではありませんが、まあそれなりです。今後、より型安全な判別共用体の提供とモナディックな弾幕構築の提供とか、弾幕コンピュテーション式...とかとか妄想しています。あとは、この型を弾幕として解釈することができる処理系を実装すればおkです(それが面倒くさい)。

というか、俺たちのF#にも牙突ください(!)

外部DSLと内部DSLを両立する

f:id:zecl:20131219212500p:plain

内部DSLであるBulletml判別共用体の構造は再帰的な構造になっていなく複雑です。上図のような構成では外部DSLを解析するためのパーサの実装コストが大きくなってしまいます。そこで、より抽象度の高い中間的なASTを用意し、下図のような構成にすることでパーサの実装コストを軽減することを考えます。

f:id:zecl:20131219212527p:plain

中間ASTとは、つまるところXmlNodeの構造そのものなので、以下のように単純な木構造の判別共用体で表現することができる。

  type Attributes = (string * string) list

  type XmlNode =
  | Element of string * Attributes * XmlNode list
  | PCData of string

外部DSLを解析するパーサは、より抽象的で単純な構造にパースするだけでよくなるので、とてもシンプルな実装で済むようになります。実際のパーサの実装例を見てみましょう。

SXML形式のパーサ

かの竹内郁雄氏は、「XMLもぶ厚いカッコのあるLisp」とおっしゃっています。
第1回 Lispの仏さま 竹内郁雄の目力

実際、XML InfosetのS式による具象表現であるところのSXMLがそれを体現していますね。

SXMLの基本的な構成要素はこんな感じです。

[1]              <TOP> ::= ( *TOP* <PI>* <Element> )
[2]          <Element> ::= ( <name> <attributes-list>? <child-of-element>* )
[3]  <attributes-list> ::= ( @ <attribute>* )
[4]        <attribute> ::= ( <name> "value"? )
[5] <child-of-element> ::= <Element> | "character data" | <PI>
[6]               <PI> ::= ( *PI* pi-target "processing instruction content string" )  

不勉強なので、Lispとかよくわかりませんが、要素はlist の car。内容は cdr。属性は @ に続く cdr という感じで表現できれば、BulletMLSXML形式で記述できるようになります。ごくごく簡易的なSXMLのパーサはFParsecを使うと次のような感じに書けます。

namespace FsBulletML

open System
open System.IO 
open System.Text 
open System.Text.RegularExpressions
open FParsec
open FParsec.Internals
open FParsec.Error
open FParsec.Primitives
open FParsec.CharParsers

module Sxml =

  type SxmlParser<'a> = Parser<'a, unit>
  type SxmlParser = Parser<XmlNode, unit>

  let chr c = skipChar c
  let skipSpaces1 : SxmlParser<unit> = skipMany (spaces1) <?> "no skip"
  let endBy p sep = many (p .>> sep)
  let pAst, pAstRef : SxmlParser * SxmlParser ref = createParserForwardedToRef()

  let parenOpen = skipSpaces1 >>. chr '('
  let parenClose = skipSpaces1 >>. chr ')'
  let parenOpenAt = skipSpaces1 >>. skipString "(@"
  let pChildOfElement = (sepEndBy pAst skipSpaces1)
  let betweenParen p = between parenOpen parenClose p
  let betweenParenAt p = between parenOpenAt parenClose p

  let pAttr = 
    let pFollowed = followedBy <| manyChars (noneOf "\"() \n\t") 
    let pLabel = manyChars asciiLetter 
    let pVal = 
      skipSpaces1 >>. chr '"' >>. 
      (manyChars (asciiLetter <|> digit <|> noneOf "\"'|*`^><}{][" <|> anyOf "()$+-*/.%:.~_" ))  
      .>> (skipSpaces1 >>. chr '"')
    skipSpaces1 .>>
    pFollowed >>. pLabel .>>. pVal

  let pAttrs = skipSpaces1 >>. sepEndBy (betweenParen pAttr) skipSpaces1 
  let pBody = skipSpaces1 >>. chr '\"' >>. manyChars (noneOf "\"") .>> chr '\"'  

  let pElement = 
      skipSpaces1 >>. (followedBy <| manyChars (noneOf "\" \t()\n")) >>.
      pipe4 (manyChars asciiLetter)
            (attempt (betweenParenAt pAttrs) <|>% [ ])
            (attempt pBody <|>% "")
            (pChildOfElement)
            (fun name attrs body cdr -> cdr |> function
            | [ ] when body <> ""  -> Element(name, attrs, [PCData(body)])
            | [ ] -> Element(name, attrs, [ ])
            | cdr -> Element(name, attrs ,cdr)) 

  let ptop = parse {
      let! car = betweenParen pElement
      return car
  }

  do pAstRef := ptop

  [<CompiledName "Parse">]
  let parse input = runParserOnString pAst () "" input
  
  [<CompiledName "ParseFromFile">]
  let parseFromFile sxmlFile = 
    let sr = new StreamReader( (sxmlFile:string), Encoding.GetEncoding("UTF-8") )
    let input = sr.ReadToEnd()
    parse input

このパーサによって、全方位弾の弾幕をこんな感じに記述できるようにます。

(bulletml
  (action (@ (label "circle"))
    (repeat
      (times "$1")
      (action
        (fire
          (direction (@ (type "sequence")) "360/$1")
          (bullet)))))
  (action (@ (label "top"))
    (repeat
      (times "30")
      (action
        (actionRef (@ (label "circle"))
          (param "20"))
        (wait "20")))))

スッキリ。ぶ厚いカッコを一掃できて、いい感じですね。

独自形式のパーサについて

「括弧も一掃したいんだが。」という人のためにインデント形式をご用意。インデントで構造化されたデータ表現と言えばYAMLがありますが、YAMLほどの表現力は必要がなくて、弾幕をシンプルに書けさえすればよいので独自形式をでっち上げてみました。

以下のような感じのインデント形式の弾幕も読み込めるパーサも用意してみました。 オフサイドルールな構文をパースしたい場合は、@htid46さんFParsecで遊ぶ - 2つのアンコールの記事がとても参考になりますね。ソースは省略します。

bulletml
  action label="circle"
    repeat
      times:"$1"
      action
        fire
          direction type="sequence":"360/$1"
          bullet
  action label="top"
    repeat
      times:"30"
      action
        actionRef label="circle"
          param:"20"
        wait:"20"

JSON形式のパーサもあってもよいのかも。

Demoプログラムで使った背景画像について

DSL繋がりということで、動画のサンプルプログラムでスクロールさせている背景画像の作成には、DSLで背景画像が作れるF#製のツール「イラスト用背景作成「BgGen」 ver 0.0.0 - ながとのソフト倉庫」を利用させてもらいました。

背景生成に使ったコマンド

rect 0 0 480 680 #f000
circles 1 0 50 #aff

これで宇宙っぽい背景が作れちゃいました。こりゃ便利。

感想

F# の判別共用体で型付きDSLをするのは無理ではないけど、段階的にケースが少なくなってどんどん絞り込まれていくような型の場合、それを解釈する処理の実装コストが大きくて結構つらぽよ感溢れました。あまりおすすめできませんね。欲しいです牙突マジ。

・頭の中ではすでに出来ている(作り方がわかっていると思っている)ことと、実際に作ってみることは、やっぱり結構なギャップがあるね。

・ 結果的に行き当たりばったりのゴリ押し実装になってしまったきらいはあるけど、判別共用体で定義した弾幕が思い通りに動いたときはちょっとした達成感が。おじさんちょっと感動しました。

GitHubに晒したソースコード。ツッコミどころ満載なのはある程度は自覚していますが、より良い方法があればアドバイスを。お気づきの点がありましたら @zecl までお願いします。


疲れた!!!でも、ものづくり楽しいし、F# 楽しい✌('ω'✌ )三✌('ω')✌三( ✌'ω')✌



あわせて読みたい

DSL開発:ドメイン駆動設計に基づくドメイン固有言語開発のための7つの提言 - Johan den Haan
言語内 DSL を考える。- togetter
GADTによるHaskellの型付きDSLの構築 - プログラミングの実験場


*1

シューティングゲームを作ったことがある人ならだいたいは勘でわかりますが