TypeProviders に関するちょっとした小ネタ集
F# Advent Calendar 2016 の 22日目の記事です。
TypeProvider
については以前、 型プロバイダー(TypeProvider)のちょっとしたアレコレというのを書きました。
書いたのはそーとー前ですが、今でも割と役に立つかもしれない以下の話題について扱っているので、気になるものがあればどうぞ。
- 型プロバイダーに渡すことができる静的引数んの種類 - 型プロバイダーの実行部分は部分的な制限がある - 他のDLLに依存する型プロバイダーを作る - 他のNuGetパッケージに依存した型プロバイダーを作ってNuGetで配布するときのやり方 - 型プロバイダーが参照するファイルの更新チェックを実装する - 消去型と生成型
さて。この記事は、TypeProvider
に関する役立つものから役立たないものまで雑多な小ネタをいくつか適当に書いていきます。何かひとつでも引っかかるものがあって、持ち帰ってもらえれたら幸いかなと思います。
TypeProviderを作る際の下準備
古くは、F# Type Provider Templateとゆーありがたいテンプレートが用意されていました!が、もう過去のもの。今は何も考えずに、まず FSharp.TypeProviders.StarterPack
を Paketなり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
に仕上げることができるかもしれません。
ちなみに想像に難しくはないと思いますが、消去型のTypeProvider
でenum
を作ろうとしても、読んで字のなんとやら型情報が消えてしまいます!ので、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に渡す静的パラメータをハードコーディングしたくないパターンのやつ
StringReader type provider https://t.co/vMbcSlvgbb たし蟹なるほどねッ!(´・_・`)
— ぜくる (@zecl) 2016年11月22日
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
を作っていると、Array
やList
の要素に対して直にアクセスするシーンが頻出します。ラムダ式の引数から静的パラメータを取り出す場合、直にインデックス指定でアクセスして値を取り出すのが割と正攻法な気がしていますが、まあある程度は割り切ってしまって、雑パターンマッチで済ませてパターンマッチの記述をさぼるのもアリかなあと思います。#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])
あるいは状況に応じて、Roslyn
やFSharp.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 をコンストラクタで呼び出すには、ProvidedConstructor
のBaseConstructorCall
でよしなに処理してあげればよいです。
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
で取得した baseMathod
をInvokeCode
内の任意のタイミングで呼び出せばたぶんできます(雑すぎ)
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
を用いて、private
なfield
に値を設定しておて、それを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
これ、実はPaket
やNuGet
を介して最新版を取得しても、まったく降ってこないんですよね。気づきにくい(!)
軽い気持ちでコードを読もうとしても黒魔術すぎてわたしのような雑魚にはぶっちゃけわけわかめ(難しい)なんですが、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で。」とお約束をしていたので、こちらにまとめます。使用したスライドをそのまま公開することも考えましたが、それだと内容が伝わりにくくてあまり価値がなさそうかなと思ったのと、変な誤解をされる恐れもあると考えたので、発表時に話した内容に簡単な補足を加えたり、ときにはざっくり省いたりしたかたちでこちらにまとめることにしました。
時間オーバーしただけあってだいぶ長いです(ポエム)。
セッションを聞いていただいたかたは、続きのみどうぞ
@mayukiさんが作ってくれた画像。あまり深くは触れませんが、IQ145 がだいぶやばいです。別に殺伐とはしていなかったはず。
基本的には、F#が"ふつうに関数型言語である"というありがたみのない話
言語遍歴としてはVB(レガシー的なのも含む)、C#、Java、あとはDelphiあたり。その他いろいろさわってますが、よく知られている(一般的な)オブジェクト指向言語をつかって仕事をしていることが多かったです。
かつて関数型言語関連の研究をしていたとか、数学が得意だとかでもなく一般の人。
C#の会社に所属していながら、F#の話をする。どうなんだ?という感じですが、理解ある会社、同僚たちと働かせてもらえてありがたいことです。
弊社開発者、結構 F# インストールしてくれています。使ってくれている(or 今後使ってくれる)かどうかはわかりませんが。
こちらのツイートをみたとき、まゆきさんツンデレだなあと思いました。
自宅でのVSのデフォルト環境は F# にしてます(だから何)
・FsBulletML
むかし一世を風靡した(?)弾幕記述言語BulletML(ABA Games)のF#実装。
特徴としては、判別共用体による型付き内部DSLと XML、SXML、FSB(独自形式)の3種類の外部DSLを備えていることです。
また、各外部DSLから内部DSLへのコンパイル時変換を可能とする FsBulletML.TypeProviders も提供しています。NuGetである程度DLされているんだけど、使ってみたという話を一度も聞いたことないですね。
・UniFSharp
Unityエディタ上からMSBuildを実行してF#をビルドするやつ。
疑似的にF# ScriptをインスペクターにD&Dできたりします。あと、無駄にUnityちゃんがしゃべります。Unity で F# がサポートされるようになる日は、まずこなさそうですね。
F#に対して「無関心」だった人に関心をもってもらいたい。F#に対してもともと「関心」があった人をより協力者に近づけたい。というのを目標(目的に)
>どうして F# を流行らせたい?
単純に良いと思うものは広めたいし。語り合える仲間が増えるのはうれしいよね!というのは当然として、ユーザーが少ないことは問題でしかないと考えるからですね。実際ユーザが少なくてイイことなどまるでないので。開発者が少ないということは、ライブラリやフレームワークの選択肢の幅も狭くなります。書籍も出版されにくいし、Web上でも情報が少なく何かトラブルがあったときに困る可能性があります。んなもんで、ビジネスにも適用されにくくなります。とかいう、ごくふつうの話
実はVisual Studio 2010からいました
MSRとして、ML(Meta Language)的なアプローチを使った言語設計で研究することが決定したのが2002年らしいので、F#1.0に至るまで約3年かかったということになるのかな。
F#の父 ドンちゃんこと Don Syme 氏は、1998年からマイクロソフトで働き始めて.NET CLR と C#(VB) のためのジェネリック機構を作った人。すごい人 #実際すごい
F#の特徴的な機能としては、sequence expression(シーケンス式)、非同期ワークフロー、computation expressions(コンピュテーション式)、Type Provider(型プロバイダー)などの新しい(面白い)試みがある。小さなところでいうと、Active Pattern,(アクティブパターン)なんてのもある。簡単に言うとユーザ指定形式のパターンマッチ。
最新版のF#4.0は、3.0からの大きな機能拡張はなくて割と地味な印象がある。でも、細かなバグの修正だったり手触り感の向上があって普通に便利になりました。あと、デフォではVisual Studioでインストールされなくなりましたが。このあたりは別にどっちでもいいです、
他には、毛色が違うけどリアクティブプログラミングもパラダイムのひとつと言って差支えないかな。
F#は関数プログラミングに主軸を置いたマルチパラダイム言語です。もっと言うと、F# は .NET で 実用レベルで使用できる関数プログラミングのパラダイムをサポートしている唯一の言語。それ以上でもそれ以下でもないです。
C#(VB)はオブジェクト指向プログラミングを主体としたマルチパラダイム言語。とはいっても、C#(VB)のそれは関数プログラミングを支援している関数型プログラミング言語に比べて、ごくごく限られていて。C#(VB)でできるプログラミングのパラダイムの1つに関数プログラミングを加えるは、ちょっと違うかなと(個人的に)は思うところです。もちろん、LINQ使用時のラムダ式の利用など関数プログラミングのエッセンスが効果的にとりいれていて、それはオブジェクト指向プログラミングとも非常になじんでいます。
Monoがあることによって、いろんなところで動くよ
他には、GPUとかブラウザ上とか。さまざまな環境で動くF#さん
Fsharpbindingプロジェクトってのがあってですね・・・いろんなエディタいろんな環境でつかえるよ。 https://github.com/fsharp/fsharpbinding
でも、いちばんのオススメは、やはりVisual Studio(現時点ではおそらく最強)。Visual F# Power Tools 拡張機能の充実ぶりがすばらしいので。
一応条件付きとはいえ、Community Editionでフルに使えますし #いい世の中になりました
C#で作られたライブラリも利用可能、とは言ったものの、F#から使いやすいC#ライブラリとして設計されていないと、だいぶツライ感は否めないですけど。はい。
Visual F# Power Toolsの機能をいくつか紹介しました。
クイックサーチ機能ですね
VS標準ではfsproj内のフォルダ作成はサポートされていないので、
フォルダをきって、階層構造をつくれるようになるのはふつうにうれしい。
これまではインターフェイスの実装は、すべて手で記述する必要があった…(圧倒的作業感)
自動生成はありがたい(実際助かる)
レコード型のスタブを生成する機能
判別共用体のパターンマッチケースを網羅的に自動生成してくれるやつ(こちらも助かる)
必要な名前空間やモジュールを追加してくれるやつ
C#でいうところの、必要な using ほげもげ を追加してくれるやつ
発音はプラジナでよいのかな
ちなみに #prajna とはサンスクリット語でして、日本語(仏教用語)だと般若です。 #comuplus #roomd
— INOMATA Kentaro (@matarillo) 2015, 9月 26
githubですでにα版が公開されたので、興味のある人は見てみたらよいんじゃないでしょうか #か
msrccs.github.io
www.nuget.org
プログラミングスタイルと、それの支援状況的なアレ
関数型言語、関数プログラミングなどは定義が結構あいまいで説明が難しい
おおむね間違っていないと思うが、個人的な見解と雑さがやばい
副作用 is 何 という話もあるが、深入りはやばい #実際やばい
ので、ここでは破壊的代入操作や、コンソールや画面描画などの出力を伴う操作ということでお茶を濁す
高階関数の扱いやすさが、 関数型言語っぽさに比例しているかなあと、個人的には考えます。まぁ、関数型言語かどうかを端的にあらわすのであれば、ラムダ計算に基づいているかとかで判断するほうが妥当なのかなあ? 最近のプログラミング言語は、マルチパラダイム化(というか、関数プログラミングの一部の要素が取り入れられる)が進んでいてその境界が割と曖昧になってきている。が、関数プログラミングというスタイルについて
言語(コンパイラ)や開発環境でどの程度支援されているか(推奨されているか)どうかで、関数型言語と呼んで差支えがないかどうか判断をする感じかなあ。
これ。関数型妹botかなんかが流行っていた時期につぶやいたものだったかな?
オブジェクト指向プログラミング言語は、オブジェクト指向プログラミングがなんたるかを教えてくれない。
では、関数型言語はどうだろう? かなりのぶぶんで言語そのものから関数プログラミングのスタイルを学ぶことができる。
ある面では学習コストは高いけど、そういった面で考えると、頭の中がまっさらならむしろ学習コスト低いのかななんて。
理解するのに時間がかかるような難しい理論や概念ももちろんありますが、大部分が誤解かと思います。関数型言語を使って関数プログラミングを学ぶ、その入り口自体には難しい要素はほとんどなく、シンプルかつ堅牢なルールが重んじられる(ことが多い)ので複雑性が少なくなって、むしろ簡単とさえ感じる部分もあるくらいです。少なくとも、可変な値に翻弄される複雑性を減らすことができるので、書きたいプログラムそのものに集中できる時間が増える。ということは実際に利用してみると実感できるはずです。
ただし言語によっては大きくシンタックスが異なる場合があるので、ある程度の抵抗感が生まれてしまうのは事実(しょーがない)。F#も「見た目がきもくて無理」なんて言われることもある。好んで使っているひとは「美しい」って言う人多いのに。
F# は(いい意味で)いろいろとゆるいので。みゅーたぶるな値も使い放題だし、手続き的だったりオブジェクト指向なスタイルを残しつつ、少しずつ関数プログラミングのスタイルを取り入れていくことが無理なくできるのではないかな。関数型言語のなかでも、とりわけ難しくない方ではないかなあと思う(個人の感想)
だいぶ釣りだし、実際のところ1分じゃぜんぜん足りない(そりゃそうだ)
某F# for fun and profit という素敵サイトの記事から引用したものです
fsharpforfunandprofit.com
重要なところも割とさっくりと大胆に省略されていたりしますが、まあイサギヨサがある。
そもそも、F# のコードを見たことがない人も多いのでは?ということで入れました。
かなりさらっとやるつもりでいたんですが、ここにかなりの時間を費やしてしまったので時間切れになってしまった。
let キーワードで 不変な値に名前をつけて定義します
明示的に値の型を記述していないこと注意してください(型はコンパイラが推論してくれます)
F# のリストは、順序が指定されていて変更不可の一連の同じ型の要素を表します。
リストを定義するには、セミコロンで要素を区切って角かっこで囲む感じ。
単一要素とリストの結合はコロン2つで。リスト同士の結合は@で。
F# では 返り値を返すときに return キーワードを使用しません。常に関数内の最後の式が返される。
最初こそ少しとっつきにくいかもしれないが、シンプルなルールなのですぐに慣れることができるだろうし、実際明確でわかりやすい。
ふぃぼなっち
リストから偶数のみを取り出す関数
偶数かどうかを判断する isEven が関数内関数として定義されている
リストの対する操作は Listモジュールに定義されているので、よしなに使う。
List.filterはLINQでいうところのWhereのこと。
1から100までのリストについて、正方形の面積を求めてその和をだす(なんのために)
関数に適用される引数を明確にするためには括弧を用います。
この例に関していうと、括弧を書かないとコンパイルが通らないので。
F#ではコードを書きやすく、また見通し良くするためのパイプライン演算子が標準で提供されている。
無名関数(ラムダ式)を書くときは fun キーワード使う。
もしかすると楽しい気分を演出するかもしれないし、しないかもしれない
完全に一致(ではない)が、似ている。
F#ではループの代わりとして利用できる高階関数が数多く提供されている。ので、ループしようと思ったら再帰関数書かなきゃだめなの!?みたいな変な誤解は持たないでほしい。C#(VB)のLINQになじみがあれば、LINQ to Objectsを使えば for や foreach などのループ用の構文を必要としない場面が多いということはわかってもらえるはず。
LINQの場合は当然、あらかじめ定義されている拡張メソッドなりがある場合のみにメソッドチェーンとして処理を手続き的に書き下すことができる。それに対してパイプライン演算子によるチェーンでは、型さえ合えば自由に関数をチェーンできる。果てしなく。
ただ、後ろになんでも繋げられることをメリットととらえるか、はたまたデメリットととらえるか。どちらも正しい。これは、開発のスタイルや重視するプログラミングパラダイムによって意見が変わってくるところだろうと思う。関数プログラミングを主体とするF# においては、パイプライン演算子によって後続の処理を好きなだけ連ねることができるのはメリットでしかない。
3年前のツイートだった
わたしはこれでF#覚えました。これは別に冗談で言っているのではなく結構本気で。(|>)パイプライン演算子という中置演算子を用いることで、関数と引数の順序を逆にすることができる。これによって、関数適用の流れを手続き型的に書き下すことができるので、処理の流れがわかりやすくなります。F#ではこの「パイプライン演算子を使う」ことが基本でありながら、同時に最上級のプラクティスであると言っても過言ではなく。これを好んでよく使うことはおのずとF#の上達に繋がります。
複数の値を組みにするやつ。
雑に言うと、フィールド名を持たない無名クラスのようなもの
System.Tuple.Create<,>とか書かなくてよいし、Item1,Item2…と付き合わなくてもよいという話。
多くの C# 開発者は タプル扱いやすく改善されないかなぁと思っているはず。
ちなみにF#のそれ。この場合かっこ(パーレン)も省略できる
あ、匿名型は F# にはないです。 F# でもたまーに欲しいシーンはあるので、ちょっと欲しいです。
match式は雑にいうと、 switch 文の強力版
ただし、match式はパターンマッチのほんの1例にすぎない。この例だと、あまりありがたみはないが、F#にはさまざまな形式のパターンマッチが提供されているので、非常に便利。
雑に言うと、タプルの各要素(フィールド)に名前つけたやつ
名無しの権兵衛さん
レコード型をパターンマッチで分解しているの図。
突然の and !だったりするが華麗にスルーで。
リストもパターンマッチで分解できる例
他にも、ユーザ指定形式のパターンマッチであるところの、Active Pattern,(アクティブパターン)なんてのもある。
なんか見たことあるやつ。動物抽象クラスを継承して動物たちが鳴くやつ。
突然の function式 ! だが、ここもスルーで(機能的にはmatch式と同じでその引数省略できる版)
いわゆる代数的なデータ型的なやつ。一部誤解を与えてしまう可能性もあるが、ある種のクラス階層を表現するのに使える。
オブジェクト指向プログラミングの「継承」と明らかに違う点は、まったく性質の異なるものを同質のものとみなして定義することが出来る点、かなあと思う。
これを難しいと言われると、もうどうしようもない。
カーリーブレイス。まあつまり中括弧のことなわけですが。洞窟物語のキャラにカーリーブレイスって居たなと。
スコープがインデントで区切られるという構造のオフサイドルール(軽量構文)。Pythonインスパイア。
これに対して、インデントにしばられない冗語構文もあるが、(どうとでもなるので)そっちは別段覚えなくてもよいと思う(個人の感想)
オフサイドルールは最初こそ少しとっつきにくいかもしれないが、ルールがシンプルなのですぐに慣れることができるだろうし、実際明確でわかりやすいように思う。ただ、人によっては「見た目キモすぎぃぃぃ」と言う反応をする方もいる。それも理解できないこともない。慣れないものを見ると拒絶反応を示すというのはごくごく当たり前の反応だし。無理なものは無理だし。生理的に受け付けないので付き合えません!(No Thank you!)とかもまあ現実世界でも実際あるわけだし、世知辛いですね(なんの話)
唐突には挿入した息抜き用の画像。
ぐらばく(@Grabacr07) さんが作ってくれたものです。頼んでもいないのに。(写真の人物本人の使用許可あり)
かなりの謎コラだけど、非常によい表情です。
変数は、値にわかりやすい名前を付けて定義しておくことで、続くコードで再利用するためのもの。
let で変数を作り出すことを変数束縛と言います(くどい)
例えば破壊的代入(再代入)を許すようなプログラムでは、変数が異なる状態を持ち得るのでその変数を参照する場合「この変数の今の値は何だろう?」と観測しなければならなくなり、複雑性を生み出す要因となる。つまり心配ごとが増える。
let による変数束縛ではデフォルトで破壊的代入(再代入)ができません。が、mutable宣言をすることで、破壊的代入が可能な変数として宣言することもできます。しかし、関数プログラミングというスタイルでは極力「破壊的な状態の変化」というものを避ける傾向にあるので、破壊的代入をデフォルトで許さないようにしている。これはスタイルとしての「副作用をできるだけ使わない」ということを推奨/支援しているとも言える。
例えば変数が状態を持ってしまうと、その変数を参照する箇所で「この変数の今の値は何だろう?」と注意しなければならない。
この例においては、2番目の出力は「5963」ではなく「4649」と表示される。
F#は、変数を後から宣言した同名の変数でシャドウイング(隠ぺい)することができる。シャドウィングが使えることで、破壊的代入をしなくても不便さはそれほどなくなる。また、プログラムのスコープが明確になり、書きやすいだけではなく非常に読みやすいプログラムとなる。
immutableな値として、同名の変数として宣言するシャドウィングを用いる場合、
スコープの中の直前のものにのみ着目すればよいので、余計な複雑さを排除できてコードの見通しがくなり、プログラミングそのものに集中できる。
※同一モジュール上のlet 宣言をシャドウィングすることはできないという制限あり
プログラマーのかわりに関数を利用しているコードの型などから、コンパイラがよしなに型をつけてくれる型推論。関数の引数や戻り値にも型を書かなくてよいし、可能であればジェネリックな関数として自動的に推論してくれたりする。
変数の定義も関数の定義も、letというキーワードで統一的に定義できるようになっていてうれしい。
少し乱暴だが、F#では let キーワードを覚えるだけで、かなりのプログラミングが書ける。match式あるいは if 式を覚えれば条件分岐もできる。
くわえて 再帰のための let rec を覚えれば、ループ処理が書けるので、いとも簡単に三種の神器を手に入れることができる(アッハイ)
JavaScriptのコードでカリー化関数と部分適用の説明。
突然のJavaScript!なのだが、社内勉強会用に作った資料の流用だったりするので。
F# ではカジュアルに無意識にカリー化関数が書けるので、うれしいという話
高階関数のうれしいところ
高階関数を扱いやすくするのに必要な機能とかの話。
F#では関数内関数が簡単にかけてうれしい。
高階関数を書きやすくする要因のひとつとして、関数内関数が無理なくかけるかというのがある。
C# でも書けないこともないが、どうしても書きにくいので書くモチベーションにならない。
すすんで書くようなものではないことは多くの人が知っているし、C# では素直に名前を付けてメソッドとして切り出して書くべきだ。
F#では、無理なく関数内関数を書くことができるのでなんの抵抗も生まれないし、その場限りの関数であれば、関数内関数として定義することはごくごく当たり前に行われる。これは、インデントによるオフサイドルールとも相性が良く、書き方/スタイルとして非常になじむ。関数プログラミングを支援/推奨している言語では プログラミングスタイルそのものが変わります。
関数の合成のしやすさも、関数プログラミングのしやすさの指標と考えられます。C#で合成関数を書こうとするとかなり骨が折れます。しかもこれは、特定の型に関する合成しかできません。
F#で定義した関数は、コンパイラの型推論と自動ジェネリック化のメカニズムによって、可能であれば暗黙的にジェネリックな関数となります。これにより非常にシンプルな記述で、あらゆる型の関数を合成するような関数(演算子)が表現できてしまうというわけです。こういった特徴を兼ね備えているので、小さな関数を巧みに組み合わせて、より大きな関数を構築していくという関数プログラミングというプログラミングスタイルが違和感なく行えます。
レキシカルスコープのほうが、コードを追いやすい話。
ダイナミックスコープと言えば、Lisp とか?
仮にダイナミックなスコープだとすると、10 が出力される
ダイナミックスコープで解決されてうれしい場面ってどの程度あるのだろうか。罠でしかない感。
F#はレキシカルスコープを採用しているので、オフサイドルールやシャドウィングの仕様ともよくマッチしていて人道的でわかりやすいスコープとなっている。なので F# のコードは非常に読みやすい(追いやすい)。これが仮にダイナミックなスコープだとすると、関数を適用したときに、どのような結果をもたらすかコードリーディングだけで判別しにくくなってしまう。高階関数とかこわくて使えない。
高階関数の利用を容易にする条件はこんな感じかな
言い換えると、関数プログラミングしやすい条件ともいえる。
ここから、「続きはWebで」のぶぶん
有名なアレ。
VBの方ですか?Nothingもっとこわい?
C#でも null非許容型が欲しいなんて話はたびたび聞きます。
テストでカバーすればなんとかなる? そういう話ではないんです。
F# では通常、null 値を値にも変数にも使用しません。デフォルトで危険性を回避するように設計されています。例外的な場合を除いて、nullの使用はできる限り避けるよう言語設計されています。
通常F#で定義されているクラスには null は代入できません。
ムリヤリつっこうもとしてもコンパイルエラーとなります。
そうは言っても、.NET Frameworkの上にある以上 F# でも null 値を扱うことはあります。
ただし、その状況はとても限られていて
のいずれかしかありません。これらの状況に対しては AllowNullLiteral 属性を使用して対応します。
これを使用することで null 許容する型を明示的に定義することができます。
この例ではその2つの状況いずれにも当てはまらないので、 AllowNullLiteral は使用すべきではありません。
また、null チェックは強制することができないので、チェック漏れがあれば当然 NullReferenceExceptionとなります。
ちなみに、F#で定義されていてF#からのみ使用する型の場合、null 値を作成する方法は、 Unchecked.defaultof または Array.zeroCreate のいずれかの関数を使用した場合に限られます。
とはいえ、値がない状態というのをプログラミングで表現したいシーンはたくさんあります。そんなときに役立つのがオプション型で、これにより「値がないかもしれない」ということを型として明示することができます。計算不能や計算失敗などにより、実際の値が存在しない場合などに使えます。null の厄介な点は null であることが型の情報として現れないことで。nullは参照型であればどんな型の値にもなり得るので、
null参照によるエラーというのは実行してみるまで誰にも分からないからです。つまり、nullが来るかどうかの判定をプログラマーが正確に管理しなければならないこと意味します。これは実際難しいことです。
オプション型を導入することで、 null値を使用することなく安全に「値がない状態」を表すことができます。
関数プログラミングとは直接は関係ないところですが、F# のとても面白い(エキサイティングな)機能なのでご紹介します。
「インフォメーションリッチプログラミングから LINQ を引いて残った物が 型プロバイダーだよ。」「わけがわからないよ。」
TypeProviderのドキュメントによると、「基本的に定型のデータあるいはサービス情報空間をF#プログラミング内に導入することを目的としています 」とのこと。コンパイル時に型のない情報に型を与えることで、F# コード上であらゆる情報を安全に扱えるようにする仕組み。
型プロバイダーの主目的はコンパイル時プログラミングではないものの、型のないものを扱う際に生じやすいしょーもないバグを未然に防ぐことができるソリューションと解釈することもできます。型のないものを安全に扱える世界があなたの手中にといったところです。
ごむごむ
いろいろできそうです(オラワクワクしてきたぞ)
でも、TypeProviderはDSLなどをメタプログラミングするためのものではないと、TypeProviderのドキュメントに書かれています。
ですがが、まぁ無視してしまって全然よい気はしますね(実際にさまざまなTypeProviderでかなり無視されている)
特に学習目的であればメタプログラミング目的の方がとっつきやすいかもしれませんので。
NuGetからTypeProviders.StarterPack を導入するのがもっとも近道。
TypeProviderを作る際に必要な便利部品がひととおり用意されている。
スキーマが動的に変化することは想定されていないので、スキーマがコーディング中やプログラムの実行中に安定しているかということは、もちろん前提として必要。
ここで言う .NET の型システムへのマッピングとは、つまりクラス だったり 構造体だったりに対するマッピング。F# の型システムと言っているのは、主に判別共用体やレコード型に対するマッピングを意味している。関数プログラミングとの親和性を考えて、F# の型システムの型を生成するのか、あるいはオブジェクト指向プログラミングになじみやすいような型を生成するのか、そのあたりはよく検討してデザインする必要がある。
関数プログラミングとの親和性をとる場合の例でいうと、判別共用体は再帰的な構造を表現するのにとても適正しているので、冒頭で紹介した弾幕記述言語の F# 実装であるところの FsBulletMlでは実際に、XML(外部DSL)から判別共用体(型付き内部DSL)への安全なマッピングのためのTypeProvider提供しています。
セッションでは、いちおう簡単なDemoだけ紹介しました。
コンパイル時にメタデータからリアルタイムで型がつくられる。Visual Studioとも連携しているので、インテリセンスも効くという感じ。
こんな簡単なDemoからでも、「TypeProviderでなんか面白いことができそうだな!」ということが容易に想像できると思う。
今回のDemoのような単純なものであれば少し勉強すれば、すぐに作れるようになりますが、ある程度複雑なものになると、ぐんと難しくなります。
ただ、それは関数プログラミング的な側面で難しいという意味ではなく、メタプログラミングの側面として難しいという意味です。
TypeProviderの作り方について学ぶには、日本語情報に限らず海外の情報もまだまだ少ないので、
FSharp.Data等のライブラリのソースを読んで勉強する感じになるかと思います。
github.com
F#に限らずさまざまな言語の話も飛び交う感じの割とフリーダムなゆるい集まり。
私も何度かお邪魔して楽しくすごさせていただいています。ほとんどF#を書いたことがないというような人でも気軽に参加していただける勉強会です。
F#に詳しい参加者も多いので、やさしいF#erにマンツーマンでわからないことをいろいろ教えてもらえる(かも!?)
日本全国のF#er の集まり(online)という位置づけ(のはず)
gitterで F# に関する情報交換などがなされています gitter.im
英語ですが、F#に関するさまざまな情報がまとまっています(無料会員登録可)
特に言語の発展に物申したい場合は有料会員になる感じだと思います。
F# Software Foundation
@yukitos さんは、こちらのボードメンバーでもあります。
他のプログラミング言語に比べてF#が特別難しいというようなことはありません(もちろん難しい部分も当然あります)。
F#は高階関数が扱いやすく関数プログラミングがしやすい言語です。なので、プログラミングのスタイルは自然と関数プログラミングに軸をおいたものとなります。
よく、F#って何に使えるんですか?等の質問をいただきますが、VBがC#とほとんど同じ場面で使えると同じように、ほとんどの場面で同じようにF#も使えます。F#を選択する理由があるとするならば、関数プログラミングのスタイルおよびそれによって享受できるメリットを重視するかしないかくらいのものです。それ以上でもそれ以下でもありません。関数プログラミングのスタイルを重視しないのであれば、選ぶ理由はないでしょう。
現状、世間一般ではそれを重視しないのが多数派であり、人材の確保が難しかったりある程度学習コストが高いという側面があるので、選択肢としてあがることも少なく(んなもんで人材が増えないという悪循環)、結果「流行っていない」ということになります。
手続き寄りの一般的なプログラミングを習得している人に対して、抽象化の道具として新たに関数プログラミングを軸にすることを提案することは
実際間違いではないし、どちらかというと筋がよいことだと思うんだけど、それを必要としている人もいるししてない人もいるというのは理解できます。で、実際いまのところは(残念ながら)必要とされていない(らしい)ので、F#流行っていないということになります。大変良い言語(実際)ですが、今のところMSが推す気配なし!ということもあって、だいぶ闇は深いです。
長々と(ポエム)書きました(しゃべりました)が、少しでも興味を持ってもらえたなら、うれしいな #うれしいな
F# Build Tools for Unity(ゲームのやつ) - UniFSharpのご紹介
これは F# Advent Calendar 2014の延長戦、 30 日目の記事です。
書いたきっかけ
@zecl ML Advent カレンダーに書いてくださいよーw
F#の人達は知ってるけど、MLな人は知らないとかあるかもしれないですし。
— h_sakurai (@h_sakurai) 2014, 12月 10
結局、25日に間に合いませんで。ゆるふわ #FsAdvent に急遽参加しました。そんなわけで、ML Advent Calendar 2014も合わせてどうぞ。 この記事は非常に誰得でニッチな内容を扱います。ほとんどの場合役には立たないでしょう。F# らしい成分もあまりありませんので、まあ適当に流してください。
UniFSharpとは?
UniFSharpは、私が無職だった(ニートしていた)ときに作成した Unityエディタ拡張 Assetです。割と簡単に導入することができます。
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のゲーム開発がしやすくなります。いわゆる、"作ろうと思えば作れるの知ってるけど、面倒くさくて誰もやらなかったことをやってみた系のツール"です。まぁ、実際やるといろいろ大変。また、オープンソースヒロインのユニティちゃん(ユニティ・テクノロジーズ・ジャパン提供)をマスコットキャラクターに採用しました。ユニティちゃんの音声で時報やイベント、ビルド結果の通知を受けられるという、本来の目的とはまったく関係のない機能も提供しています。
UniFSharpはUnityのEditor拡張です。 ← わかる
Unity Editor から F# Scriptを作成できます。 ← わかる
ユニティちゃんの音声でビルド結果の通知する機能を提供しています。 ← ????
https://t.co/opuuLaNPdT
— みずぴー (@mzp) 2014, 8月 15
Unityを使うならまぁ当然 C# 一択です。がまぁ、趣味で使う分にはフリーダム。
1000 人 Unity ユーザがいるとすると、その中の 4 人は Boo ユーザらしい (戦慄)
— たけしけー (@takeshik) 2014, 9月 4
1000 人 Unity ユーザがいるとすると、その中の 0.01 人は F# ユーザーかもね(適当)
ちなみに、UniFSharp自体も F# で書かれています(ちょっとC# Scriptが混ざってます)。そう、基本的には F#で Unity のほとんどの部分(エディタだろうがゲームだろうが)を書くことができます。この記事では、UniFSharpが提供する機能および、それがどのように実装されているのかについて書きます。ここで紹介でもしないと、GitHubのリポジトリを誰も覗いてくれることもないでしょうし。ハイ。
ご利用の際は、まぁいろいろあると思います(お察し)。覚悟しましょう。
ExecutionEngineException: Attempting to JIT compile method 'Microsoft.FSharp.Core.FSharpFunc`\
誰だよF# がUnityで使えますとか言ったの
— 広告収入欲しさのユーチューバー (@tikal) 2014, 7月 12
この記事を読むその前に...むろほしりょうたさんの初心者がF#をUnityで使ってみた!という記事をオススメします。
F# Scriptの作成
Unityのエディタ拡張では、カスタムメニューを簡単に作ることができます。
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を生成するという機能を提供しています。
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ウィンドウ上に表示させることができます。
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
ファイルがぶっ壊れて開けなくなっちゃいますからね。
@zecl 詳しくは知らんですが、実際は階層構造でなく単に属性的に所属するフォルダとしてフォルダ名をヒモづけられてるのではと予想(´・ω・`)
— omanuke (@omanuke) 2014, 6月 25
@zecl @omanuke @haxe http://t.co/j9KW2eN9qt
Folder機能を作った本人も何故駄目なのかわからないって言ってますね。
実際 <Compile Include="1\2\1\1.fs" /> とかするとエラーになるんですがどこが悪いのか…
— yukitos (@yukitos) 2014, 6月 26
Inspectorで F# コードのプレビューを表示
Inspectorウィンドウで F#コードのプレビューを表示するためには、カスタムエディタを作成します。ただし、カスタムエディタはDLLのみでは実装を完結することができないため(謎の制約)、C# Scriptで。
http://forum.unity3d.com/threads/editor-script-dll-and-regular-script-dll-not-adding-custominspector-scripts.107720/
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# 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
UnityからEnvDTE叩こうとするとUnityが死ぬ^p^
— ぜくる (@zecl) 2014, 6月 25
これはきな臭い...。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# プロジェクトの構成の詳細を設定できます。細かい説明は省きます(雑。
ユニティちゃんの機能もろもろ
ユニティ・テクノロジーズ・ジャパンが無償で提供してくれているユニティちゃんのAsset に同封されている多彩な音声。せっかくあるので使ってみたい。特に「進捗どうですか?」とか使わない手はない。そういや、いわるゆる萌え系だとか痛い系のIDEって結構あるけど、しゃべる感じのやつってあんまりないよなぁ。とかいうのが一応実装動機ということで。
- ・起動時ボイス(ON/OFF)
- ・ビルド時ボイス(ON/OFF)
- ・進捗どうですか?(ON/OFF, 通知間隔指定あり)
- ・時報通知のボイス(ON/OFF)
- ・イベント通知のボイス(ON/OFF)
- ・誕生日のお祝い(ON/OFF, 日付を指定)
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エディタ上で、音声ファイルを再生したい系の人は上記のような感じのをひとつこさえておけば、ハカドルかもね。
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も利用できますし、めしうま状態必至。
あ、fsharp .org に VS2013 Tools for Unityを使えば、F# で書いたDLLをデバッグ実行できるYOとしれっと書いているが、誰得? http://t.co/YHUiTdqSCI
— ぜくる (@zecl) 2014, 12月 11
おまけ
Unityえふしゃーぷまん達
http://t.co/Fdux8rbZCb
FSharpをUnityでも使えるんだろうけど、dll作って、アセットフォルダに突っ込んで読み込みかぁ。さらさらっと出来ると嬉しいなぁ。しかし、やる気がないw
— h_sakurai (@h_sakurai) 2014, 12月 10
UnityをF# で使おう委員会
— 切れ痔 (@lmdexpr) 2014, 9月 1
ヤッター! UnityでFSharp動いたあぁー!!!
— c000 :: RealWorld → (@c255) 2014, 5月 27
unitychan2Dを F# へ移植してみた https://t.co/qV0YI1NkVh #fsharp #unitychan pic.twitter.com/5G2DT4V6Ci
— ぜくる (@zecl) 2014, 7月 12
Unityもっと安くならないかな……。あとついでにスクリプトがF#に対応してくれないかな……
— 徹人 (@t_tetsuzin) 2014, 6月 20
当然、Unity でオール http://t.co/kfcpCbcFQ2 で書くことも可能(誰得すぎて誰もやらない
— ぜくる (@zecl) 2014, 2月 18
「F# のコードをDLLにビルド→Unityプロジェクトに自動でコピー→MonoBehaviourのラッパーを自動生成」で快適なF# (on Unity)生活が送れるかと思ったけどImportAssetの待ち時間イライラするかも
— よだ (@n__yoda) 2014, 1月 28
@hamazy @florets1 @kazuhito_m 空いてたら僕は発表します。最近UnityとF# を使ってるのでそういうのはいいかなと思ってます。
— ダニー (@tataminomusi) 2014, 11月 6
UnityでF#使えるんなら使ってやらんこともない(上から目線)
— 自然界 (@mizchi) 2014, 5月 6
次期メジャーバージョンアップで「Unity、F# を開発言語としてサポート」とかならないかなぁ。API がそぐわないとか色々あるんだろうけど…
— Yu I. into VR (@japboy) 2014, 7月 24
えふしゃーぷでUnityは理想的には可能(趣味の範囲で)
— ぜくる (@zecl) 2014, 6月 29
"Unity によりサポートされないコンパイラを使用できる場合があり(例えば F#)" http://t.co/v0xX4sgUxS なので全部F#でイケるが、いろいろ制約あって謎のノウハウが必要なのでメリットよりもデメリットが多いので素人にはお勧めできない
— ぜくる (@zecl) 2014, 5月 17
意外といらっしゃる。もちろんこれですべてではない。
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)の型を付ける型プロバイダーの提供も考えていたが、型プロバイダーでは判別共用体を作ることができないという理由から、このライブラリでの型プロバイダーの提供は一時保留としていた。
で、
型プロバイダで判別共用体作れない件。自分がやりたかったこととは直接関係なかったことに今更気づいて('A`)ウボァー
— ぜくる (@zecl) 2014, 8月 9
大きな思い違いをしていたと気が付いた。型プロバイダーによってメタデータを元に"判別共用体の型そのものは作ることはできない"が、"型プロバイダーで作成した型が持つメソッドやプロパティを通じて、メタデータを元に作成した判別共用体を返すことはできる"ということに(気付くの遅すぎ)。
FsBulletMLで型プロバイダーを提供してみよう
ということで、FsBulletMLで型プロバイダーを提供してみることにした。
FsBulletMLで提供している3つの外部DSL(XML
形式、SXML
形式、FSB
形式)をBulletml判別共用体(内部DSL)に型付けしてくれる型プロバイダーを提供したい。FsBulletMLでは、もともとFsBulletML.Core
とFsBulletML.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
に束縛した値をうまく参照することができないため、以下のようなエラーとなる
なんてことはない。直接コードクォート内に記述するとうまくいく。
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 型プロバイダーを利用する側のコードもコンパイルが通るので良さそうに見える。
しかし、実行すると次の例外が発生する。当然と言えば当然だが型プロバイダーの実行時に public ではない module を参照することはできないからである。
かといって、 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()
コンパイルが通るし一見良さそうに見えるが、この型プロバイダーを利用しようとすると以下のようになる。
型プロバイダーのコンパイル時に参照できている 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# 言語サービスがそのプロバイダーを無効化するようにシグナルを通知することができる。シグナルが通知されると、型プロバイダーが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()
型が消えてますね。
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()
ポイントは、ProvidedTypeDefinition
をIsErased = false
とすること。
ProvidedAssembly
で一時アセンブリを作り、そのアセンブリにAddTypes
で生成する型を登録することです。
この例では、アセンブリ内に定義したPiyo
クラスを継承する型を生成しています。
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
クラスが型プロバイダーによって生成されていることが確認できます。
MonadBuilderTypeProvider構想
— ふ''れいす (@bleis) 2014, 7月 30
モジュール名を渡すと、そのモジュールにbindやらreturnやらが定義されていた場合にそれを引っ張ってきてビルダークラスを作るようなTypeProvider
— ふ''れいす (@bleis) 2014, 7月 30
うほ!とても面白いアイデア。
生成型の型プロバイダーだと確かにそういった構想の面白いブツが作れそうですね(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バグを未然に防ぐことができるソリューションと言うこともできる。型のないものを安全に扱える世界があなたの手中に的な。
型プロバイダーさん「ALL YOUR TYPES ARE BELONG TO US」 カッコイイ…! #fsugjp
— 黒曜 (@kokuyouwind) 2014, 8月 3
型プロバイダーさんステキ!抱いて!
この世のあらゆるものに型を付けた男、型王タイプロジャー。彼の死に際に放った一言は人々を型へと駆り立てた。「型が欲しい? 欲しけりゃくれてやる。作れ! この世の全ての型はここに集まる!」男達は型プロバイダーを作り夢を追い続ける。世はまさにインフォメーションリッチプログラミング時代!
— ぜくる (@zecl) 2014, 8月 13
型プロバイダーの作成って難しそう(実際難しい)
一応それなりに F# を追っかけているマンなので、某サンプルの「Hello World TypeProvider」くらいはチョロっとかじりました。
でも、特に作りたいオリジナルのネタもなくって(あるいは既に先駆者がいたりで)、なかなか触れる機会に恵まれまれずにコンニチに至ります(言い訳)。そういう人多そう。実装したいネタがどうこうというより、そもそも難しかったり取っつきにくかったりで手が出しずらいという説もある。もし詳細を知りたいなら FSharp.Data等のライブラリのソース嫁がデフォというなかなかに漢らしい感じだし。何か作りながら覚えようにも、予期せぬエラーとそのメッセージに悩まされることも多いし、デバッグ実行で地道に追っても原因がよくわからず途方に暮れるなんてこともあり...結構ハードルは高い。実際上級者向けではある(個人の感想)。
「型プロバイダーってアレでしょ?作るもんじゃなくて、使うもんでしょ?」
難しいことはよくわからないけど、F# のすごい人たちが「"型のないもの"に"型を付けてくれる"」とっても便利でクールなライブラリを作ってくれていて、「オレのような一般 F# ユーザーは利用者としてその恩恵を受けることができればいいよね。」くらいに割り切って押さえておくだけでもよいという代物かもしれない。
TypeProviderを作る場面にぶち当たらない…というか作ってまで幸せになる場面にぶち当たらない(´・ω・`)
— omanuke (@omanuke) 2014, 8月 14
ごもっともすぎるご意見。汎用的で有用な型プロバイダーを自分で実装するような機会は、あまりないような気がする(便利そうなやつはもう既に誰かが作ってくれていることが多いし)。概要だけ把握しておいて、もし実装する機会があったらそのときじっくり調べながら頑張ればいいんじゃないだろうか。必要に応じて。
F# 上級者です!(ドヤァ #型プロバイダーわかりません
— いげ太 (@igeta) 2013, 11月 29
【朗報】型プロバイダーは華麗にスルーしても上級者になれる(!)
逆に言うと、型プロバイダーがわかったからって上級者にはなれません。
型プロバイダーの作成って面白そう
2014年8月3日に F# Meetup in TokyoおよびそのPart1という素晴らしいイベントがあり、fsugjp がやまだかつてない程の盛り上がりがあったようです。そのイベントで@kos59125さんによるTypeProviderに関する発表があり、更に後日@bleisさんによる、TypeProviderについて、勝手に補足がありました。素晴らしい。私は参加できませんでしたが、F# Meetup in Tokyo #fsugjp - Togetterまとめで雰囲気だけ味わいました。
触発されて何かしら型プロバイダーを作りたくなった。軽い気持ちで...、とりあえず役に立たない系型プロバイダーを作ってみた。
プロジェクトコンテンツから読み込めるようにしておきますね https://t.co/iefUg5eDDw @zecl ちょっと草植えときますね型言語Grass型プロバイダーhttps://t.co/JyneEYfunf … 書いてみた。Grassを安全に...
— Tomoaki Masuda (@moonmile) 2014, 8月 6
@moonmileさんにフィードバックを頂きました。どうもありがとうございます。
TypeProviderはProvidedType.fs(か、StartarPack)を使えばいいということだけ覚えておけばあとはなんとか。 #暴言
— yukitos (@yukitos) 2014, 8月 13
ゼロから自分で実装することも不可能ではありませんが...基本的にはこの方法しかないといっても過言ではないので、なにかしらのProvidedType.fs(の類)を使いましょう。
役に立つ立たないは置いておいて勉強したい場合はとりあえずなんか作ってみる。そうじゃやない場合は概要だけ抑えて華麗にスルーする。でいいと思います。
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 2013の20日目です。
遅ればせながら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"])]) |
共用体ケース 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
| 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
共用体ケース Direction.Direction: DirectionAttrs option * string -> Direction
--------------------
type Direction = | Direction of DirectionAttrs option * string
完全名: FsBulletML.DTD.Direction
共用体ケース Speed.Speed: SpeedAttrs option * string -> Speed
--------------------
type Speed = | Speed of SpeedAttrs option * string
完全名: FsBulletML.DTD.Speed
共用体ケース 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
共用体ケース Times.Times: string -> Times
--------------------
type Times = | Times of string
完全名: FsBulletML.DTD.Times
FsBulletMLリリースしました
弾幕記述言語BulletML
のF#
実装、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のようなアイディアも実装しやすくなる、と。
今週土曜のCLR/H勉強会 http://t.co/Wsb4ddWyA2 で「ちょ、構成ファイルの書式に今どきXMLなんてありえないでしょww」「yamlかjsonだろ」「つーか.rbで構成書くよねふつー」とかXMLのdisり大会やってみたい。そこで「でもね...」#clrh86
— jsakamoto (@jsakamoto) 2013, 11月 6
XMLのdisり大会。こちら結局どうなったんでしょう。気になります(´・_・`)
DTD定義を判別共用体で表現する
いにしえからの言い伝えによると、「判別共用体は、ドメイン知識をモデル化するための簡潔かつタイプセーフな方法である。」らしいです。
ということで、BulletML
のDTD定義を内部DSLとして判別共用体で表現する。ということについて考えてみたい。
以前、こんなやり取りがありました。
判別共用体って、
type Hoge =
| A of Hoge.B * Hoge.C
| B of string
| C of int
みたいに書けないわけで。泥臭くならざるを得ない。
そりゃそうだろうっちゃーそりゃそうなのだが(´・_・`)つらぽよ #fsharp
— ぜくる (@zecl) 2013, 7月 8
@gab_km そうなんですよ。どうしても泥臭くなってしまぅぅ...(´・ω・`)
— ぜくる (@zecl) 2013, 7月 8
@igeta 先の例は単純化したものでして。例としては微妙でしたね; twitterではうまく状況を説明しにくいのですが、やりたいのはそういうことではないのです。
— ぜくる (@zecl) 2013, 7月 8
@gab_km 確かにそんなに多くはないんですが、型に厳格目というか、ちょっとコッたことをやろうと思うと…。先の例は単純化したもので、例としては微妙でした;
— ぜくる (@zecl) 2013, 7月 8
@igeta ですです。そんな感じです。説明しなくても通じたー!(ぇ 何個も型を定義してどんどん煩雑化していくんですorz ある程度型を曖昧にすべきか。煩雑化してでも型に厳格にすべきか。設計次第といったところですが、悩ましいです^^;
— ぜくる (@zecl) 2013, 7月 8
@igeta どちらかというと代数的データ型が(◞‸ლ)
— ぜくる (@zecl) 2013, 7月 8
@igeta GADTsとかよくわかってない系おじさんですが、何か。
— ぜくる (@zecl) 2013, 7月 8
言語内に型付きDSLを構築したいようなケースでは、GADT(Generalised Algebraic Datatypes)が欲しくなるようです。つまりこれ、抽象構文木なんかを型付きで表したいときに発生する事案です。しかし、F#
にはHaskell
のGADTs
に相当するものはありません。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です(それが面倒くさい)。
牙突 RT @314maro GADTsってどう読むんだろう
— 星のキャミバ様 (@camloeba) 2013, 11月 22
というか、俺たちのF#にも牙突ください(!)
外部DSLと内部DSLを両立する
内部DSLであるBulletml判別共用体の構造は再帰的な構造になっていなく複雑です。上図のような構成では外部DSLを解析するためのパーサの実装コストが大きくなってしまいます。そこで、より抽象度の高い中間的なASTを用意し、下図のような構成にすることでパーサの実装コストを軽減することを考えます。
中間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 という感じで表現できれば、BulletMLをSXML
形式で記述できるようになります。ごくごく簡易的な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の構築 - プログラミングの実験場