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からは以上です。
VB、C#、F# それぞれのインデクサ。F# コンパイラのソースを読んで。
F# コンパイラのソースを読んで
はぇ〜さん(@haxe) とtwitterにて、以下のようなやり取りがありました。
ところで #fsharp の for m in Regex.Matches(input, pattern) do () はGetEnumerator()をしてくるわりに、m : Match に解決してくれるのはなんでだろう? 仕組みがわからにあ
2013-06-20 11:03:05 via web
@haxe F# さんがベターな型をよしなに探してくれているからですね。このあたりのソースからたどって全体を見てみると「にやり」とできるかも(!) URL
2013-06-20 11:48:37 via Silverbird M to @haxe
@zecl IEnumerable<T>を実装してないのに、どうやって見つけてるのかな?って話なんです。githubは見てますよん
@haxe 貼っ付けたリンク先のtypecheckerのソースの、"リフレクションでMethInfoを見ている" から"ではなくてですか?
2013-06-20 12:02:53 via Silverbird M to @haxe
@haxe そうだったんですね。見てみてください?
2013-06-20 12:09:49 via Silverbird M to @haxe
@zecl コード読みました。ハードコーディングのグロい実装で成り立ってるんですね?。 Hashtableなんかitemの戻り値Value側がobject型だからそれに解決されるとかかなりやばいw URL
2013-06-20 13:02:28 via web to @zecl
@zecl .NET的に正しくはDefaultMemberAttributeを見るべきなんですけどね! そうすればXmlNodeListもItem()メソッドではなくItemOfプロパティがインデクサだとわかるので。
2013-06-20 13:04:39 via web to @zecl
for m in Regex.Matches(input, pattern) do () で、 MatchCollection から Match に型が解決される件は、わたしがツイートしたとおり。typecheckerのソースを見ればわかるので、まあよし。
問題はその後。
@zecl .NET的に正しくはDefaultMemberAttributeを見るべきなんですけどね! そうすればXmlNodeListもItem()メソッドではなくItemOfプロパティがインデクサだとわかるので。
2013-06-20 13:04:39 via web to @zecl
@haxe IndexerNameAttributeで別名を付けられることも考慮して...と。確かにそれも一理ありますね。でも、IndexerNameAttributeはそもそも直接利用が推奨されていない属性ですし、インデクサに別名をつけるためのモノではなく、
2013-06-20 14:53:50 via Silverbird M to @haxe
@haxe あくまで他の言語(VB と F#)のインデックス付きプロパティと同等の機能をを提供するためのものにすぎないので。そう考えると、愚直とはいえあの実装でハズしてはいないのかなぁと。
2013-06-20 14:54:13 via Silverbird M to @haxe
@zecl IndexerNameではなくDefaultMemberで。expr.[idx]ではDefaultMemberをちゃんと見てるので "abc".[1] でもCharsプロパティにアクセスできるようです。 URL
2013-06-20 15:10:30 via web to @zecl
@haxe すいませんちゃんと読みきれていませんでした; なるほどです。勝手に整理すると、そもそもインデクサを含む型にDefaultMember属性は指定できないので、C# については考える必要がなかった。XmlNodeListのような型、あるいは
2013-06-20 15:54:45 via Silverbird M to @haxe
@haxe 他の言語(VB と F#)で、Itemという名前ではないプロパティに対して任意の名前がIndexerNameで指定されていて且つ、その任意の名前がDefaultMemberに指定されている場合が考慮されてる実装になっていたんですね。くどくどと失礼しました;
2013-06-20 15:55:01 via Silverbird M to @haxe
@zecl なんか噛み合わないと思ったら。IndexerNameはソースコードのインデクサに付ける属性ですが、コンパイル時に消されます。コンパイル後のアセンブリには、IndexerNameの有無にかかわらずTypeに対してDefaultMemberが自動生成されます。
2013-06-20 16:45:31 via web to @zecl
@haxe ですです。DefaultMemberが自動生成されるときにIndexerNameで指定している場合はその名前が使われるので。それ分の考慮がなされているのだなと。
2013-06-20 16:51:01 via Silverbird M to @haxe
読み返してみると、わたしの発言が支離滅裂なところがあり。そのせいもあって、話が噛み合っていなさすぎてやばい!(はぇ〜さんごめんなさい!)。自分の中ではだいたいわかって納得したような気になっていた・・・のです(ぇ しかし、github の fsharp のソースをまじまじと見ていると、なんだか少しモヤっするものがあったので、改めて F# コンパイラのソースを舐め回すように見てみた。実際にコードも書いて確かめてみた。
すると...、いろいろ間違い(勘違い)をしていたようです(はずかしい)。C# と VBのインデクサの仕様、 C# と F# の仕様はそれぞれ異なるということは把握していたのだが、VB と F# のインデックス付きプロパティ(インデクサ)の挙動に差があることは把握できていませんでした。F# では、DefaultMemberAttribute が自動生成されないケースがあるんです。 ΩΩΩ<な、なんだってー!?
C# のインデクサ と VB のインデクサ
「C# のインデクサ」と 「VB のインデックス付きプロパティ(インデクサ)」については、岩永さんの「インデクサー(C# によるプログラミング入門) - ++C++」を参照で(丸投げ)。
C#のインデクサのサンプル:その1
public class IndexerSample1 { private int[] arr = new int[100]; public int this[int index] { get { if (index < 0 || index >= 100) { return 0; } else { return arr[index]; } } set { if (!(index < 0 || index >= 100)) { arr[index] = value; } } } }
var sample = new IndexerSample1(); var attrs = System.Attribute.GetCustomAttributes(sample.GetType()); // DefaultMemberAttributeが自動生成される Debug.Assert(attrs.Any(x => x.GetType () == typeof(DefaultMemberAttribute))); var attr = attrs.Where(x => x.GetType() == typeof(DefaultMemberAttribute)).Cast<DefaultMemberAttribute>().FirstOrDefault() ; Debug.Assert(attr.MemberName == "Item");
特に細工をすることなくC#でインデクサ付きの型を作ると、"Item"というメンバ名で DefaultMemberAttribute が自動生成される。問題なし。
C#のインデクサのサンプル:その2
public class IndexerSample2 { private int[] arr = new int[100]; [System.Runtime.CompilerServices.IndexerName("SpecialItem")] public int this[int index] { get { if (index < 0 || index >= 100) { return 0; } else { return arr[index]; } } set { if (!(index < 0 || index >= 100)) { arr[index] = value; } } } }
var sample = new IndexerSample2(); var attrs = System.Attribute.GetCustomAttributes(sample.GetType()); // DefaultMemberAttributeが自動生成される Debug.Assert(attrs.Any(x => x.GetType() == typeof(DefaultMemberAttribute))); var attr = attrs.Where(x => x.GetType() == typeof(DefaultMemberAttribute)).Cast<DefaultMemberAttribute>().FirstOrDefault(); // IndexerNameAttributeで指定された名前で生成されている Debug.Assert(attr.MemberName == "SpecialItem");
IndexerNameAttributeで指定した "SpecialItem" というメンバ名で DefaultMemberAttribute が自動生成される。問題なし。
C#のインデクサのサンプル:その3
[System.Reflection.DefaultMember("SpecialItem")] public class IndexerSample3 { private int[] arr = new int[100]; [System.Runtime.CompilerServices.IndexerName("SpecialItem")] public int this[int index] { get { if (index < 0 || index >= 100) { return 0; } else { return arr[index]; } } set { if (!(index < 0 || index >= 100)) { arr[index] = value; } } } }
インデクサ付きの型に対して DefaultMember属性を明示的に指定することはできない。コンパイラに怒られます。
MSDNライブラリ - DefaultMemberAttribute クラス
http://msdn.microsoft.com/ja-jp/library/system.reflection.defaultmemberattribute(v=vs.110).aspx
プロパティは、そのプロパティに引数が存在し、かつ、プロパティ名またはそのいずれかのアクセサーが DefaultMemberAttribute で指定された名前と一致する場合、インデクサー (Visual Basic では既定のインデックス付きプロパティ) としてインポートされます。 格納している型に DefaultMemberAttribute が存在しない場合、その型にはインデクサーは存在しません。 C# コンパイラでは、インデクサーを含むすべての型について、DefaultMemberAttribute を出力します。 C# では、既にインデクサーが宣言されている型に対し、直接 DefaultMemberAttribute で属性を指定するとエラーになります。
MSDNにもそう書いてある。
続いて、VB のインデックス付きプロパティ(インデクサ)について見ていく。
VB のインデックス付きプロパティのサンプル:その1
Public Class IndexerSample4 Private arr As Array = New Integer(100) {} Default Public Property Item(ByVal index As Integer) As String Get If index < 0 OrElse index >= 100 Then Return 0 Else Return arr(index) End If End Get Set(ByVal Value As String) If Not (index < 0 OrElse index >= 100) Then arr(index) = Value End If End Set End Property End Class
var sample = new IndexerSample4(); var attrs = System.Attribute.GetCustomAttributes(sample.GetType()); // DefaultMemberAttributeが自動生成される Debug.Assert(attrs.Any(x => x.GetType() == typeof(DefaultMemberAttribute))); var attr = attrs.Where(x => x.GetType() == typeof(DefaultMemberAttribute)).Cast<DefaultMemberAttribute>().FirstOrDefault(); Debug.Assert(attr.MemberName == "Item");
特に細工をすることなくVBでインデクサ付きの型を作ると、C# と同様に "Item"というメンバ名で DefaultMemberAttribute が自動生成される。問題なし。
VB のインデックス付きプロパティのサンプル:その2
Public Class IndexerSample5 Private arr As Array = New Integer(100) {} <System.Runtime.CompilerServices.IndexerName("SpecialItem")> _ Default Public Property Dummy(ByVal index As Integer) As String Get If index < 0 OrElse index >= 100 Then Return 0 Else Return arr(index) End If End Get Set(ByVal Value As String) If Not (index < 0 OrElse index >= 100) Then arr(index) = Value End If End Set End Property End Class
var sample = new IndexerSample5(); var attrs = System.Attribute.GetCustomAttributes(sample.GetType()); // DefaultMemberAttributeが自動生成される Debug.Assert(attrs.Any(x => x.GetType() == typeof(DefaultMemberAttribute))); var attr = attrs.Where(x => x.GetType() == typeof(DefaultMemberAttribute)).Cast<DefaultMemberAttribute>().FirstOrDefault(); Debug.Assert(attr.MemberName == "Dummy");
DefaultMember属性が暗黙的に生成されるが、IndexerName属性で指定した "SpecialItem" という名前は無視される。実際のプロパティ名(DisplayName)である"Dummy" で作られる。これは知ってた。
VB のインデックス付きプロパティのサンプル:その3
<System.Reflection.DefaultMember("Hoge")> _ Public Class IndexerSample6 Private arr As Array = New Integer(100) {} <System.Runtime.CompilerServices.IndexerName("SpecialItem")> _ Default Public Property Item(ByVal index As Integer) As String Get If index < 0 OrElse index >= 100 Then Return 0 Else Return arr(index) End If End Get Set(ByVal Value As String) If Not (index < 0 OrElse index >= 100) Then arr(index) = Value End If End Set End Property End Class
DefaultMember属性に、インデクサのプロパティ名と異なるメンバ名が指定されると競合が発生する。こんなの初めて書いたw
VB のインデックス付きプロパティのサンプル:その4
<System.Reflection.DefaultMember("Item")> _ Public Class IndexerSample7 Private arr As Array = New Integer(100) {} <System.Runtime.CompilerServices.IndexerName("SpecialItem")> _ Default Public Property Item(ByVal index As Integer) As String Get If index < 0 OrElse index >= 100 Then Return 0 Else Return arr(index) End If End Get Set(ByVal Value As String) If Not (index < 0 OrElse index >= 100) Then arr(index) = Value End If End Set End Property End Class
DefaultMember属性に、インデクサのプロパティ名と同様のメンバ名を指定すると問題ない。C# とは異なり、VB ではインデクサ付きの型でDefaultMember属性を明示的に指定することが可能。
var sample = new IndexerSample7(); var attrs = System.Attribute.GetCustomAttributes(sample.GetType()); // DefaultMemberAttributeが指定されているので、当然存在する Debug.Assert(attrs.Any(x => x.GetType() == typeof(DefaultMemberAttribute))); var attr = attrs.Where(x => x.GetType() == typeof(DefaultMemberAttribute)).Cast<DefaultMemberAttribute>().FirstOrDefault(); Debug.Assert(attr.MemberName == "Item");
DefaultMemberAttributeが明示的に指定されているので、当然存在する。メンバ名もそのまんま。ここまでは問題ないです。
F# のインデックス付きプロパティ(インデクサ)
F#のインデックス付きプロパティのサンプル:その1
type IndexerSample8 () = let arr : int [] = Array.zeroCreate 100 member this.Item with get(index) = if index < 0 || index >= 100 then 0 else arr.[index] and set index value = if not (index < 0 || index >= 100) then arr.[index] <- value
var sample = new IndexerSample8(); var attrs = System.Attribute.GetCustomAttributes(sample.GetType()); // DefaultMemberAttributeが自動生成される Debug.Assert(attrs.Any(x => x.GetType() == typeof(DefaultMemberAttribute))); var attr = attrs.Where(x => x.GetType() == typeof(DefaultMemberAttribute)).Cast<DefaultMemberAttribute>().FirstOrDefault(); Debug.Assert(attr.MemberName == "Item");
F#のインデックス付きプロパティのサンプル:その2
type IndexerSample9 () = let arr : int [] = Array.zeroCreate 100 [<System.Runtime.CompilerServices.IndexerName("SpecialItem")>] member this.Item with get(index) = if index < 0 || index >= 100 then 0 else arr.[index] and set index value = if not (index < 0 || index >= 100) then arr.[index] <- value
var sample = new IndexerSample9(); var attrs = System.Attribute.GetCustomAttributes(sample.GetType()); // DefaultMemberAttributeが自動生成される Debug.Assert(attrs.Any(x => x.GetType() == typeof(DefaultMemberAttribute))); var attr = attrs.Where(x => x.GetType() == typeof(DefaultMemberAttribute)).Cast<DefaultMemberAttribute>().FirstOrDefault(); Debug.Assert(attr.MemberName == "Item");
C#と異なり、VBと同じ挙動。知ってた。そりゃそーですよね。
F#のインデックス付きプロパティのサンプル:その3
[<System.Reflection.DefaultMember("Item")>] type IndexerSample10 () = let arr : int [] = Array.zeroCreate 100 [<System.Runtime.CompilerServices.IndexerName("SpecialItem")>] member this.Item with get(index) = if index < 0 || index >= 100 then 0 else arr.[index] and set index value = if not (index < 0 || index >= 100) then arr.[index] <- value
var sample = new IndexerSample10(); var attrs = System.Attribute.GetCustomAttributes(sample.GetType()); // DefaultMemberAttributeが自動生成される Debug.Assert(attrs.Any(x => x.GetType() == typeof(DefaultMemberAttribute))); var attr = attrs.Where(x => x.GetType() == typeof(DefaultMemberAttribute)).Cast<DefaultMemberAttribute>().FirstOrDefault(); Debug.Assert(attr.MemberName == "Item");
VBと同じように、明示的にDefaultMember属性を指定することができる。
F#のインデックス付きプロパティのサンプル:その4
[<System.Reflection.DefaultMember("Hoge")>] type IndexerSample11 () = let arr : int [] = Array.zeroCreate 100 [<System.Runtime.CompilerServices.IndexerName("SpecialItem")>] member this.Item with get(index) = if index < 0 || index >= 100 then 0 else arr.[index] and set index value = if not (index < 0 || index >= 100) then arr.[index] <- value
VBと同じように、競合が発生してエラーとなるかと思いきや・・・、コンパイルが通る!!!
var sample = new IndexerSample11(); var attrs = System.Attribute.GetCustomAttributes(sample.GetType()); // DefaultMemberAttributeが自動生成される Debug.Assert(attrs.Any(x => x.GetType() == typeof(DefaultMemberAttribute))); var attr = attrs.Where(x => x.GetType() == typeof(DefaultMemberAttribute)).Cast<DefaultMemberAttribute>().FirstOrDefault(); Debug.Assert(attr.MemberName == "Item");
DefaultMember属性のメンバ名に誤った名称が指定されている場合、それは無視される。実プロパティ名をメンバ名として DefaultMember属性 が自動生成される。これは予想外の動き!!!
しかし、この場合
let sample = new IndexerSample11() let v = sample.[0]
なぜか、DefaultMember属性のメンバ名が"Hoge"だって言われる。なので、インデクサにアクセスできない。ど、どういうことだってばよ!?
確認のためのソースが悪かったorz
var sample = new IndexerSample11(); var attrs = System.Attribute.GetCustomAttributes(sample.GetType()); // DefaultMemberAttributeが自動生成される Debug.Assert(attrs.Any(x => x.GetType() == typeof(DefaultMemberAttribute))); var attrs2 = attrs.Where(x => x.GetType() == typeof(DefaultMemberAttribute)).Cast<DefaultMemberAttribute>(); Debug.Assert(attrs2.Any( x => x.MemberName == "Hoge"));
DefaultMember属性が暗黙的に生成されているんだけど、明示的に指定したやつとだぶっちゃっている。あらまあ。
ってかこれ、バグっぽいちゃーバグっぽいゼ!?
F#のインデックス付きプロパティのサンプル:その5
type IndexerSample12 () = let arr : int [] = Array.zeroCreate 100 [<System.Runtime.CompilerServices.IndexerName("SpecialItem")>] member this.Dummy with get(index) = if index < 0 || index >= 100 then 0 else arr.[index] and set index value = if not (index < 0 || index >= 100) then arr.[index] <- value
var sample = new IndexerSample12(); var attrs = System.Attribute.GetCustomAttributes(sample.GetType()); // DefaultMemberAttributeが自動生成されない Debug.Assert(!attrs.Any(x => x.GetType() == typeof(DefaultMemberAttribute))); var attr = attrs.Where(x => x.GetType() == typeof(DefaultMemberAttribute)).Cast<DefaultMemberAttribute>().FirstOrDefault(); Debug.Assert(attr == null);
IndexerSample5のように、VBと同じ挙動をするかと思いきや、なんと、DefaultMemberAttributeが自動生成されないケースがここで発生。これは、F#のインデクサ付き型で、DefaultMemberAttributeが自動生成されないケースが存在すると言うよりも、「F# は、VB とは挙動が異なり、任意の名称のプロパティではでインデクサ付きの型とはならない」と言うのが正しいだろう。なんということでしょう。F# コンパイラによって「DefaultMemberAttributeが自動生成されない」ケースがあった!!!これは知らなかった。意図的なのかどうなのかわからないが、C#、VB いずれとも異なる挙動になるように作られている。
ってかこれ、バグっぽいちゃーバグっぽいゼ!?
F# でインデクサにアクセスすることができない。当然、こうなります。
F# コンパイラのソースを見てみようのコーナー
では、コンパイラの中で一体何が起こっているんでしょう。ソースを見てみる。
IL的に DefaultMemberAttribute を自動生成しているらしいこのあたりを引用する。
https://github.com/fsharp/fsharp/blob/master/src/fsharp/ilxgen.fs#L6239
let defaultMemberAttrs = // REVIEW: this should be based off tcaug_adhoc_list, which is in declaration order tycon.MembersOfFSharpTyconSorted |> List.tryPick (fun vref -> let name = vref.DisplayName match vref.MemberInfo with | None -> None | Some memberInfo -> match name, memberInfo.MemberFlags.MemberKind with | ("Item" | "op_IndexedLookup"), (MemberKind.PropertyGet | MemberKind.PropertySet) when nonNil (ArgInfosOfPropertyVal cenv.g vref.Deref) -> Some( mkILCustomAttribute cenv.g.ilg (mkILTyRef (cenv.g.ilg.mscorlibScopeRef,"System.Reflection.DefaultMemberAttribute"),[cenv.g.ilg.typ_String],[ILAttribElem.String(Some(name))],[]) ) | _ -> None) |> Option.toList
「// REVIEW: this should be based off tcaug_adhoc_list, which is in declaration order」のコメントも気になっちゃいますが、それは置いていおいて・・・。ここを起点に全体のソースを舐め回すように眺めてみる。ふむふむなるほど。F# コンパイラさん、IndexerName 属性はまったく見ていないご様子。そして、「let name = vref.DisplayName」を見ればわかるように、実プロパティ名を参照している。そして、実プロパティ名が、"Item" 、"op_IndexedLookup"のいずれかの場合に限り、実プロパティ名を使って DefaultMemberAttribute が暗黙的に生成されていることがわかります。
では、DisplayNameが "op_IndexedLookup"であるとき、とはどんな時か。次のサンプルのようなケースのときである。
F#のインデックス付きプロパティのサンプル:その6
[<System.Reflection.DefaultMember("Hoge")>] type IndexerSample13 () = let arr : int [] = Array.zeroCreate 100 [<System.Runtime.CompilerServices.IndexerName("Fuga")>] member this.Hoge with get(index) = if index < 0 || index >= 100 then 0 else arr.[index] and set index value = if not (index < 0 || index >= 100) then arr.[index] <- value
var sample = new IndexerSample13(); var attrs = System.Attribute.GetCustomAttributes(sample.GetType()); // DefaultMemberAttributeが自動生成される Debug.Assert(attrs.Any(x => x.GetType() == typeof(DefaultMemberAttribute))); var attrs2 = attrs.Where(x => x.GetType() == typeof(DefaultMemberAttribute)).Cast<DefaultMemberAttribute>(); Debug.Assert(attrs2.Any( x => x.MemberName == "Hoge"));
DefaultMemberAttributeはダブっていない。もちろんIndexerName属性も華麗にスルーです。
当然、インデクサにF#からアクセスできる。
自分のためのまとめ
・F# コンパイラは、DefaultMember属性を暗黙的に生成するとき、C#とは異なり、IndexerName属性は華麗にスルーされる。VBと同じ。
・F#のインデックス付きプロパティと、VBのインデックス付きプロパティは違う。思い込みイクナイ。
・F# コンパイラは、インデックス付きプロパティがあっても DefaultMember 属性を暗黙的に生成しない場合がある(てゆーか、それインデクサ付きの型じゃないですしおすし)。
・VB、C#、F# は、それぞれインデクサの仕様が異なるので気をつけましょう。
・ってかこれ、バグっぽいちゃーバグっぽいゼ!?(DefaultMember属性のダブりとかマジやべぇ)*1
気まぐれでサラッとだけ書くつもりだったのに。なんやかんやで無駄に長くなって疲れた(内容しょぼいのに!)。
お疲れ様でした。
*1:まぁ、ふつーにインデクサを使うだけなら問題にならないので、「仕様です」っちゃー仕様ですね
Retry Monad for Transient Fault Handling (Topaz + FSharpx)
4月14日に札幌で行われた第69回CLR/H勉強会にて、「Retry Monad for Transient Fault Handling - F#とWindows Azure と私 -」と題して、ライトニングトークで発表しました。
Microsoft Enterprise Library 5.0 Integration Pack for Windows Azure(EL5 for Azure)のTopaz および FSharpx を利用してモナドを作りました。Topazを利用する理由は、再利用可能な再試行戦略およびWindows Azure向けの検出戦略が組み込み済みであり、それをそのまま利用したいからです。EL5 for AzureはOSSなので、どのような実装がなされているか実際に確認することができるので、すべてをF#で書き直すこともできますが、それでは車輪の再発明になってしまいます。Retry Monad for Transient Fault Handling は、一時的障害が発生するかもしれない計算について、それぞれ異なるRetryPolicyを適用しながら再試行処理を行います。一時的な障害に対するリトライ処理をひとつの計算として包括的に扱うことができるモナド実装です。このRetryモナドの計算結果は、Choice<’T1,’T2>型で得ることができ、これによりFSharpxで定義済みの Eitherモナドで扱うこともできます。
Retry Monad for Transient Fault Handling
namespace Monad.Retry open System [<AutoOpen>] module Retry = // #r "Microsoft.Practices.TransientFaultHandling.Core" // #r "FSharpx.Core" open Microsoft.Practices.TransientFaultHandling open FSharpx [<Sealed>] type TransientErrorCatchAllStrategy () = interface ITransientErrorDetectionStrategy with member this.IsTransient (ex : exn) = true [<Sealed>] type TransientErrorIgnoreStrategy () = interface ITransientErrorDetectionStrategy with member this.IsTransient (ex : exn) = false let defaultRetryStrategyName = "DefaultRetry" let defaultRetryCount = 3 let defaultMinBackoff = TimeSpan.FromSeconds(3.0) let defaultMaxBackoff = TimeSpan.FromSeconds(90.0) let defaultDeltaBackoff = TimeSpan.FromMilliseconds(30.0) let (<+) (rp:RetryPolicy<'TResultStrategy>) retrying = rp.Retrying |> Event.add(retrying) type RetryPolicies = static member NoRetry() = new RetryPolicy<TransientErrorIgnoreStrategy>(0, TimeSpan.Zero) static member Retry<'TTransientErrorCatchStrategy when 'TTransientErrorCatchStrategy : (new :unit -> 'TTransientErrorCatchStrategy) and 'TTransientErrorCatchStrategy :> ITransientErrorDetectionStrategy>(retryCount : int , retryInterval : TimeSpan) : RetryPolicy<'TTransientErrorCatchStrategy> = new RetryPolicy<'TTransientErrorCatchStrategy>(retryCount, retryInterval) static member Retry<'TTransientErrorCatchStrategy when 'TTransientErrorCatchStrategy : (new :unit -> 'TTransientErrorCatchStrategy) and 'TTransientErrorCatchStrategy :> ITransientErrorDetectionStrategy>(retryCount : int , initialInterval : TimeSpan, increment : TimeSpan) : RetryPolicy<'TTransientErrorCatchStrategy> = new RetryPolicy<'TTransientErrorCatchStrategy>(retryCount, initialInterval, increment) static member Retry<'TTransientErrorCatchStrategy when 'TTransientErrorCatchStrategy : (new :unit -> 'TTransientErrorCatchStrategy) and 'TTransientErrorCatchStrategy :> ITransientErrorDetectionStrategy>(retryStrategy : RetryStrategy) : RetryPolicy<'TTransientErrorCatchStrategy> = new RetryPolicy<'TTransientErrorCatchStrategy>(retryStrategy) static member RetryExponential<'TTransientErrorCatchStrategy when 'TTransientErrorCatchStrategy : (new :unit -> 'TTransientErrorCatchStrategy) and 'TTransientErrorCatchStrategy :> ITransientErrorDetectionStrategy>(retryCount : int , deltaBackoff : TimeSpan) : RetryPolicy<'TTransientErrorCatchStrategy> = let retryStrategy = new ExponentialBackoff(defaultRetryStrategyName, retryCount, defaultMinBackoff, defaultMaxBackoff , deltaBackoff) new RetryPolicy<'TTransientErrorCatchStrategy>(retryStrategy) static member RetryExponential<'TTransientErrorCatchStrategy when 'TTransientErrorCatchStrategy : (new :unit -> 'TTransientErrorCatchStrategy) and 'TTransientErrorCatchStrategy :> ITransientErrorDetectionStrategy>(retryCount : int , minBackoff : TimeSpan, maxBackoff : TimeSpan, deltaBackoff : TimeSpan) : RetryPolicy<'TTransientErrorCatchStrategy> = let retryStrategy = new ExponentialBackoff(defaultRetryStrategyName, retryCount, minBackoff, maxBackoff, deltaBackoff) new RetryPolicy<'TTransientErrorCatchStrategy>(retryStrategy) static member RetryDefault(?retryCount : int) : RetryPolicy<TransientErrorCatchAllStrategy>= let retryCount = defaultArg retryCount defaultRetryCount RetryPolicies.RetryExponential<TransientErrorCatchAllStrategy>(retryCount, defaultMinBackoff, defaultMaxBackoff, defaultDeltaBackoff) type Retry<'TResult> = Retry of (Lazy<unit -> 'TResult * LastException option>) and RetryResult<'TResult> = Choice<'TResult, LastException> and LastException = exn let exnHandler e = Retry(lazy(fun () -> Unchecked.defaultof<'TResult>, e |> Some)) type RetryBuilder (policy : RetryPolicy) = new(?retryCount : int, ?retrying) = let policy = let retryCount = defaultArg retryCount defaultRetryCount RetryPolicies.RetryDefault(retryCount) retrying |> function | None -> policy <+ (fun e -> printfn "%s" (sprintf "RetryPolicyName:%s, CurrentRetryCount:%d, LastException.Message:%s, Delay:%A" policy.RetryStrategy.Name e.CurrentRetryCount e.LastException.Message e.Delay)) | Some retrying ->policy <+ retrying RetryBuilder(policy) member this.Bind (m : Retry<'TResult>, bind : ('TResult) -> Retry<'UResult>) : Retry<'UResult> = Retry(lazy(fun () -> m |> function | Retry f -> f.Force() |> fun cont -> cont() ||> fun r _ -> r |> bind |> function | Retry f -> f.Force() |> fun cont -> policy.ExecuteAction(Func<_>(fun () -> cont() ||> fun r _ -> r,None)))) member this.Return (value : 'TResult) : Retry<'TResult> = Retry(lazy (fun () -> policy.ExecuteAction(L.F<_>(fun () -> value, None)))) member this.ReturnFrom (m : Retry<'TResult>) : Retry<'TResult> = m member this.Delay (f: unit -> Retry<unit -> 'TResult>) : Retry<unit -> 'TResult> = Retry(lazy (fun () -> policy.ExecuteAction(L.F<_>(fun () -> f() |> function | Retry f -> f.Force() |> fun cont -> cont() ||> fun f _ -> f(), None)) ||> fun r _ -> (fun () -> r), None)) member this.Zero () : Retry<'TResult> = this.Return(Unchecked.defaultof<'TResult>) member this.Combine(comp1:Retry<'TResult>, comp2:Retry<'TResult>) = this.Bind(comp1,(fun r -> comp2)) let retry = new RetryBuilder() open Operators let inline returnM x = returnM retry x let inline (>>=) m f = bindM retry m f let inline (=<<) f m = bindM retry m f let inline (<*>) f m = applyM retry retry f m let inline ap m f = f <*> m let inline map f m = liftM retry f m let inline (<!>) f m = map f m let inline lift2 f a b = returnM f <*> a <*> b let inline ( *>) x y = lift2 (fun _ z -> z) x y let inline ( <*) x y = lift2 (fun z _ -> z) x y let inline (>>.) m f = bindM retry m (fun _ -> f) let inline (>=>) f g = fun x -> f x >>= g let inline (<=<) x = flip (>=>) x let (|RetryResult|) = let rec result (r:RetryResult<'TResult>) = match r with | Choice1Of2 v -> v, None | Choice2Of2 e -> Unchecked.defaultof<'TResult>, Some(e) result let run (retryCont : Retry<unit -> 'TResult>) : RetryResult<'TResult> = try retryCont |> function |(Retry f) -> f.Force()() ||> fun r e -> e |> function |Some e -> e |> Choice2Of2 |None -> r() |> Choice1Of2 with e -> e |> Choice2Of2
一時的な障害:Windows Azure(クラウド)アプリケーションを開発するにあたって対処しなければならない課題のひとつ
他のクラウドサービスに依存するようなクラウドアプリケーションを開発する場合に開発者が対処しなければならない課題の一つに、“一時的な障害” があります。インフラストラクチャレベルの障害であったり、ネットワークの問題など一時的な条件のために発生する恐れのある障害のことです。この一時的に発生しうる障害は、ほとんどの場合は短い間隔で(ほんの数ミリ秒後に)リトライ処理を行うことで回避することができます。たとえば、Windows Azureで SQL Azureプラットフォームを利用する場合。SQL Azureサービスは、共有リソース上で大規模なマルチテナントデータベースとしてサービスが提供されるので、データベースを利用するすべての利用者に対して良好なエクスペリエンスを提供しなければなりません。そのため、SQL Azureは過剰なリソースの使用や、実行時間の長いトランザクションの発行された場合など、さまざまな理由でサービスへの接続数を抑制して、利用者が意図しないタイミングで接続を切断することがあります。これが、SQL Azureを利用した場合に生じる一時的な障害ということになります。このような障害が発生した場合であってもシームレスなユーザーエクスペリエンスを提供するために、Windows Azureアプリケーション(クラウドアプリケーション)では、一時的な障害によって処理が中断された場合にはリトライを試みるようにアプリケーションを実装する必要があります。
Microsoft Enterprise Library 5.0 Integration Pack for Windows Azureを利用する
一時的な障害に対応するアプリケーションを実装する場合、Microsoft Enterprise Library 5.0 Integration Pack for Windows Azure(以降 EL5 for Azure)を利用するのが有効です。EL5 for Azureは、マイクロソフトの pattern & practice チームによる、マイクロソフト製品やテクノロジを基として、アプリケーションを構築する上でのパターンやベストプラクティスを集めたライブラリの Windows Azure向けの拡張パックです。この拡張ライブラリが提供されるまでは、一時的障害を検知してリトライ処理を行う実装を開発者自身がおのおので組み込まなければなりませんでした。EL5 for Azureには、Transient Fault Handling Application Block (Topaz)という、Windows Azureのプラットフォームに含まれるサービス利用時に発生するさまざまな一時的な障害からWindows Azureアプリケーションを回復させるためのアプリケーションブロックが提供されています。これは、Windows Azure固有の一時的な障害のみならず、オンプレミスアプリケーションで発生するさまざまな一時的な障害に対するリトライ処理についても利用可能なように設計されており、リトライ処理について高いレベルで抽象化されたアプリケーションブロックです(Microsoft.Practices.TransientFaultHandling.Core.dllにまとめらえている)。特に、Windows Azureに特化した組み込みの実装については、SQL Azure、Windows Azure ストレージサービス、Windows Azure サービスバス、Windows Azure キャッシングサービス向けの検出戦略がそれぞれ提供されていて、Microsoft.Practices.EnterpriseLibrary.WindowsAzure.TransientFaultHandling.dllに含まれています。検出戦略と再試行戦略
検出戦略は、ITransientErrorDetectionStrategyインターフェイスを実装して作成することができます。public interface ITransientErrorDetectionStrategy { bool IsTransient(Exception ex); }
例外を引数で受け取り、その例外の種類や内部的なメッセージなどを判断して、リトライ処理を行うときは true、 リトライをせずに無視するときは falseを返すように実装するだけの非常にシンプルなインターフェイスです。Windows Azureの一時的な障害に対する4つの組み込み検出戦略として、SqlAzureTransientErrorDetectionStrategy、StorageTransientErrorDetectionStrategy、ServiceBusTransientErrorDetectionStrategy、CacheTransientErrorDetectionStrategyが提供されています。
再試行戦略は、RetryStrategy抽象クラスを継承して作成することができます。
public abstract class RetryStrategy { public static readonly int DefaultClientRetryCount = 10; public static readonly TimeSpan DefaultClientBackoff = TimeSpan.FromSeconds(10.0); public static readonly TimeSpan DefaultMaxBackoff = TimeSpan.FromSeconds(30.0); public static readonly TimeSpan DefaultMinBackoff = TimeSpan.FromSeconds(1.0); public static readonly TimeSpan DefaultRetryInterval = TimeSpan.FromSeconds(1.0); public static readonly TimeSpan DefaultRetryIncrement = TimeSpan.FromSeconds(1.0); public static readonly bool DefaultFirstFastRetry = true; public static readonly RetryStrategy NoRetry = new FixedInterval(0, DefaultRetryInterval); public static readonly RetryStrategy DefaultFixed = new FixedInterval(DefaultClientRetryCount, DefaultRetryInterval); public static readonly RetryStrategy DefaultProgressive = new Incremental(DefaultClientRetryCount, DefaultRetryInterval, DefaultRetryIncrement); public static readonly RetryStrategy DefaultExponential = new ExponentialBackoff(DefaultClientRetryCount, DefaultMinBackoff, DefaultMaxBackoff, DefaultClientBackoff); protected RetryStrategy(string name, bool firstFastRetry) { this.Name = name; this.FastFirstRetry = firstFastRetry; } public bool FastFirstRetry { get; set; } public string Name { get; private set; } public abstract ShouldRetry GetShouldRetry(); }
基本的な実装は、GetShouldRetryメソッドをオーバーライドし、リトライすべきタイミングか否かを表すShouldRetry デリゲートを返すように実装します。
public delegate bool ShouldRetry(int retryCount, Exception lastException, out TimeSpan delay);
ShouldRetry デリゲートは、リトライする回数と最後に発生した例外およびリトライを行うタイミングの遅延間隔を受け取り、リトライ処理を行うべきタイミングか否かを返します。組み込みで、Incremental(再試行と再試行間の増分の時間間隔数を制御する戦略)、FixedInterval(再試行と一定間隔の再試行間を制御する戦略)、ExponentialBackoff(指数関数的な遅延を計算するためのバックオフ戦略)が提供されています。
Transient Fault Handling Application Block (Topaz)によるリトライ処理の基本的な利用方法
Transient Fault Handling Application Block (Topaz)による基本的な利用方法(C#)は、検出戦略と再試行戦略を組み合わせて、RetryPolicyオブジェクトを作成し、そのRetryPolicyオブジェクトにリトライ中の処理を適宜設定し、RetryPolicyオブジェクトのExecuteActionメソッドを呼び出します。ExecuteActionメソッドへは、リトライを行いたい対象の処理を継続渡しスタイルで渡します。
var strategy = new Incremental("Incr1",10, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1)); var policy = new RetryPolicy<SqlAzureTransientErrorDetectionStrategy>(strategy); policy.Retrying += (_, e) => { Console.WriteLine("{0:HH:mm:ss.fff} RetryCount: {1}, ErrorMessage: {2}, StackTrace: {3}", DateTime.Now, e.CurrentRetryCount, e.LastException.Message, e.LastException.StackTrace); }; var result = policy.ExecuteAction(() => { // SQL Azureへごにょごにょ return "クエリの結果などを返す"; });
EL5 for Azureはオブジェクト指向プログラミングで書かれているライブラリ、FSharpxは関数プログラミングで書かれているライブラリです。これら異なるパラダイムの部品を組み合わせてモナドを作る。とっても面白いですね。
モナドとは
モナドは単なる自己関手の圏におけるモノイド対象だよ。何か問題でも? - フィリップ・ワドラー
圏論を少しかじったことがある人にとっては問題ない説明なのですが、そうではない場合「日本語でおk」と言わざるを得ません。
この説明だけでは少々乱暴すぎるので、MSDN - コンピューテーション式(F#)へのリンクと、F#とモナドの関係について参考になりそうな表を置いておきます。
コンピュテーション式 (F#)
http://msdn.microsoft.com/ja-jp/library/dd233182(v=vs.110).aspx
Haskell | F# | 数学(圏論) |
---|---|---|
return | return | η(単位元:unit) |
>>= | bind | (*)operator |
型クラスMonadのインスタンスであるように実装する | コンピューテーション式で少なくとも Return と Bind の2つのmemberを実装する | NA |
Monad | Computation Expression, Workflow | モナドはKleisliトリプルと等価な定義。F# と Haskell の中で定義されるモナドの構造は実際にKleisliトリプル。 |
functor through a type class definition | usually not mentioned | 関手(functor) |
function | function (fun) | 射(morphism) |
Haskellのデータ型のHask圏 | .Netデータ型の圏 | グループ、位相、グラフ、微分幾何学 |
composable functions | composable functions | 2項演算とモノイド |
MSDN - Code Recipe - F#によるモナドの実装方法とモナド則を確認するユニットテスト。 Retry Monad for Transient Fault Handling
モナド則を確認するためのユニットテスト等を含む、このプログラムコードのソリューションファイル一式を、MSDN - Code Recipe よりダウンロードすることができます。
http://code.msdn.microsoft.com/F-Retry-Monad-for-35ee1e72
関連記事
快刀乱麻を断つモナド - F#とIOモナドとコンピューテーション式の奥義とhttp://d.hatena.ne.jp/zecl/20110703/p1
knockout.jsを利用したMVVMによるコンポーネント作成。WebGridの基本的な使い方と、Pagerヘルパーの利用。
MSDN - Code Recipeに初投稿しました。
WebGridに関するまとまった説明や、ページングに関するTipsもなぜか少なかった気がしたので、書いてみました。サンプルコードを書いただけで力尽きたので細かい説明はありませんが、詳細については以下をご覧ください。
knockout.jsを利用したMVVMによるコンポーネント作成。WebGridの基本的な使い方と、Pagerヘルパーの利用。
http://code.msdn.microsoft.com/knockoutjsMVVMWebGridPager-e675c134
いまさらASP.NET MVCのモデルバインダ入門あれこれ。MEFのカスタムエクスポートプロバイダーは設計の幅を広げる。自動拡張型カスタムモデルバインダプロバイダーを作ろう。
ASP.NET MVC4 Betaがリリースされまして、WebAPIいいね!な今日この頃。誰が言ったか、これから求められるIT技術は、Web、クラウド、関数型言語の三本柱らしいです。とは言っても、世の中にはさまざまな技術が溢れています。.NETerなわたしは月並みですが、ASP.NET MVC、Windows Azure、F#を追いかけるつもりです。まぁ、日進月歩の業界ですし、わたし自身飽きっぽかったりするので来年には違うことを言っているかもしれません。最近の私はと言えば、月9ドラマ「ラッキーセブン」でメカオタ少女茅野メイ役を演じている入来茉里さんのファンになりました。スピンオフドラマの「敷島☆珈琲〜バリスタは見た!?〜」も面白い。これからブレイクすること間違いありません。
それはさておき、ASP.NET MVC関連の記事はだんだんと増えてきていますが、なぜか基本中の基本であるカスタムモデルバインダですとか、カスタムモデルバインダプロバイダーに関する記事があまりにも少ない。少なすぎて困っているASP.NET MVC入門者も少なくないと聞いています(要出典)。誰かの役に立つかもしれないということで、いまさらながらASP.NET MVC3のモデルバインダ入門あれこれについてちょっと書いておきます。
このエントリーの主な話題。わりと盛りだくさん。
・カスタムモデルバインダについて
・カスタムモデルバインダプロバイダーについて
・Base64でシリアル化可能なモデルと、その汎用モデルバインダについて
・カスタムモデルバインダでアノテーション検証を有効にする
・MEFのカスタムエクスポートプロバイダーについて
・MEFを用いた自動拡張型カスタムモデルバインダプロバイダーについて
・IModelBinderProviderインターフェイスがイケてない説
この記事のサンプルコード一式はSkyDriveへあげておきます。
すてきなモデルバインダ
ASP.NET MVC にはモデルバインダという仕組みがあり、比較的新しいMVCフレームワークで採用されていて、たとえばJavaScript製のMVCフレームワークなんかでもよく採用されているデータバインド手法です。ASP.NET MVCでは、モデルバインダと呼ばれるクラスでリクエストデータ等を使って厳密に型付けされたオブジェクトを作成して、ルーティングやクエリ、フォームパラメータなどに、コントローラーのアクションに対するパラメータの型とのバインディングが管理されます。同名のパラメータについてデータバインドを試みてコントローラのアクションを単純化してくれるし、コントローラー内に「値の変換を行う」というノイズとなる処理がなくなるので、開発者はコントローラー本来の役割の実装に集中できるようなります。素敵ですね。モデルバインディングを実際に実行するのはSystem.Web.Mvc.IModelBinderを実装したクラスで、既定ではSystem.Web.Mvc.DefaultModelBinderクラスが適用されます。この既定で動作するバインダは、文字や数値など.NETで扱う基本的な型や、アップロードされたファイルなど様々な型に対応しています。小規模またはシンプルなシナリオでは、この既定のモデルバインダが自動的に基本的な型をバインドしてくれるので、この動作について特別意識することはあまりないでしょう。ただ、世の中そんなにあまくないのが現実です。大規模または複雑なシナリオでは、既定のバインディングでは十分ではないこともあるでしょう。そのような場合、カスタムモデルバインダ(ModelBinderの拡張)を作成することになります。
既定のモデルバインダが実際にどんな働きをしてくれるのかを一目でわかるように書くと、
[HttpPost] public ActionResult Create() { var customer = new Customer() { CustomerId = Int32.Parse(Request["customerId"]), Description = Request["description"], Kind = (CustomerKind)Enum.Parse(typeof(CustomerKind), Request["kind"]), Name = Request["name"], Address = Request["address"] }; // … return View(customer); };
既定のDefaultModelBinderが処理できる範囲内であれば、上記のような煩雑な型の変換処理をまったく書かなくてよくて、下記のようにシンプルに書けるようになります。
public ActionResult Create(Customer customer) { // … return View(customer); }
モデルバインダって、とてもかわいいですね。はい。って、ASP.NET MVC3を使ってプログラミングをしている人には当たり前のことでしたね。
モデルバインダの拡張
さて、「大規模または複雑なシナリオでは、既定のバインディングでは十分ではないこともあるでしょう。」と前述しました。そのようなシナリオでは、モデルバインダの拡張、すなわち独自にカスタムモデルバインダを作成することで、さまざなシナリオに対応することができます。
モデルバインダの拡張の方法としては、IModelBinderインターフェイスを実装するか、もしくはIModelBinderを実装している既定のDefaultModelBinderクラスを継承して実装します。IModelBinderインターフェイスを実装する方法の場合は、object BindModel(...)メソッドを実装するだけというシンプル設計。
DefaultModelBinderを継承して作る場合の主な拡張ポイントとしては以下のものがあり、適宜必要なものをオーバーライドして実装します。
object BindModel(...); // モデルバインド実行 object CreateModel(...); // モデル型オブジェクト生成 bool OnModelUpdating(...); // モデル更新開始 void OnModelUpdated(...); // モデル更新完了 bool OnPropertyValidating(...); // プロパティ検証開始 void OnPropertyValidated(...); // プロパティ検証完了
また、拡張した自作のモデルバインダはいくつかの異なるレベルで登録することができて、これにより非常に柔軟にバインディング方法を選択できます。
// Application_Start()で登録する方法 ModelBinders.Binders.DefaultBinder = new CustomModelBinder(); ModelBinders.Binders.Add(typeof(MyModel), new CustomModelBinder()); // Actionの引数に属性で指定する方法 [ModelBinder(typeof(CustomModelBinder))]
他にも、ModelBinderProviderを登録して対応することもできます。これについては後程述べます。
カスタムモデルバインダを作ろう
ではカスタムモデルバインダを作成してみましょう。以下のようなユーザー定義のモデルを含む単純なViewModelをバインドしたい場合を考えます。
namespace ModelBinderSample.Models.ViewModel { public class SampleViewModel0 { public Sample0 Child { get; set; } } }
using System.ComponentModel.DataAnnotations; using ModelBinderSample.Models.ViewModel; namespace ModelBinderSample.Models { public enum Hoge { Test1, Test2, Test3 } public class Sample0 { public Hoge Hoge { get; set; } [Display(Name = "ただのプロパティ")] public string NomalProperty { get; set; } } }
IModelBinderインターフェイスを実装する方法を試してみましょう。例えば、下記サンプルのように実装することができます。object BindModel(...)メソッドの基本実装は、リクエストを適切な型に変換して返してあげる処理を書くだけです。実用性はありませんが下記サンプルのように値を直接編集したりもできますし、他にも値を検証してエラーメッセージを追加したりすることもできます。
using System; using System.Web; using System.Web.Mvc; using ModelBinderSample.Models.ViewModel; namespace ModelBinderSample.Models.ModelBinder { public class SampleViewModel0Binder : IModelBinder { public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { HttpRequestBase request = controllerContext.HttpContext.Request; var model = new Sample0() { Hoge = (Hoge)Enum.Parse(typeof(Hoge), request.Form.Get("Child.Hoge"), false), NomalProperty = request.Form.Get("Child.NomalProperty") + "だってばよ!", }; return new SampleViewModel0() { Child = model }; } } }
ビュー:Sample0/Index.cshtml
@using ModelBinderSample.Models @using ModelBinderSample.Models.ViewModel @model SampleViewModel0 @{ ViewBag.Title = "Sample0"; } <h2>@ViewBag.Message</h2> @using (Html.BeginForm("Index", "Sample0")) { @Html.TextBoxFor(vm => vm.Child.NomalProperty, new { @style = "width: 350px;" }) @Html.HiddenFor(vm => vm.Child.Hoge) <br /> <input type="submit" value="送信" /> }
コントローラー:Sample0Controller.cs
using System; using System.Web.Mvc; using ModelBinderSample.Models; using ModelBinderSample.Models.ViewModel; namespace ModelBinderSample.Controllers { public class Sample0Controller : Controller { public ActionResult Index() { ViewBag.Message = "ASP.NET MVC へようこそ"; var vm = new SampleViewModel0() { Child = new Sample0() { Hoge = Models.Hoge.Test2, NomalProperty = "うずまきナルト", } }; return View(vm); } [HttpPost] [AcceptVerbs(HttpVerbs.Post)] public ActionResult Index(SampleViewModel0 vm) { ViewBag.Message = "ASP.NET MVC へようこそ"; if (!ModelState.IsValid) { return View(vm); } return View(vm); } public ActionResult About() { return View(); } } }
モデルバインダの登録
protected void Application_Start() { AreaRegistration.RegisterAllAreas(); // Add ModelBinder ModelBinders.Binders.Add(typeof(SampleViewModel0), new SampleViewModel0Binder()); RegisterGlobalFilters(GlobalFilters.Filters); RegisterRoutes(RouteTable.Routes); }
内容はお粗末ですが、カスタマイズはできました。もう少し踏み込んだカスタマイズについては後半で。
ModelBinderProviderの拡張 : カスタムモデルバインダプロバイダー
モデルの型ごとに適切なモデルバインダを供給するクラス。それがモデルバインダプロバイダー。もっと噛み砕いて言うと、「このモデルの型の場合は、このモデルバインダを使ってバインディングしてくださいね〜」って情報を供給してくれるクラスです。カスタムモデルバインダプロバイダーは、IModelBinderProviderインターフェイスを実装して作ることができます。
SampleViewModel0モデルのカスタムモデルバインダプロバイダーを実装サンプル
SampleViewModel0BinderProvider.cs
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using ModelBinderSample.Models.ModelBinder; using ModelBinderSample.Models.ViewModel; using ClassLibrary1; namespace ModelBinderSample.Models.ModelBinderProvider { public class SampleViewModel0BinderProvider : IModelBinderProvider { public IModelBinder GetBinder(Type modelType) { if (modelType == typeof(SampleViewModel0)) return new SampleViewModel0Binder(); return new DefaultModelBinder(); } } }
このサンプルでは、型がSampleViewModel0であるとき、SampleViewModel0Binderを返し、それ以外の型のときは既定のモデルバインダを返しているだけなので、プロバイダーとしてはあまり意味がありません。通常は、さまざまなモデルの型に応じて異なるモデルバインダを返すようなモデルバインダプロバイダーを作ります。
モデルバインダプロバイダーの登録
protected void Application_Start() { AreaRegistration.RegisterAllAreas(); // Add ModelBinderProvider ModelBinderProviders.BinderProviders.Add(new SampleViewModel0BinderProvider()); RegisterGlobalFilters(GlobalFilters.Filters); RegisterRoutes(RouteTable.Routes); }
Base64でシリアル化可能なモデルと、その汎用モデルバインダ
もう少し踏み込んだカスタムモデルバインダの例を見てみます。例としてはあまりよろしくはないですが、こういう実装もできるんだよというサンプルとして、Base64でシリアル化可能なModelをバインドするための汎用的なモデルバインダを作ってみましょう。例えば、ViewModelにユーザー定義の型のプロパティを含むような場合、当然 DefaultModelBinder ではそのような型をバインドできませんので、コントローラーのアクションパラメータとうまくバインドできずに、そのViewModelのプロパティにはnullが設定されてしまいます。そこで任意の型についてBase64形式でシリアル化可能なモデルをバインドするような、汎用的なカスタムモデルバインダを考えてみます。
ひどく曖昧な抽象化ですが、まずシリアル化可能なモデルであることを表すインターフェイスを定義します。BindTypeプロパティでは、バインドする型(つまりはモデル自身の型)を返すように実装します。ToStringメソッドでは、Base64エンコードした文字列を返すように実装します。
ISerializableModel.cs
using System; namespace ClassLibrary1 { public interface ISerializableModel { Type BindType { get; } string ToString(); } }
そのインターフェイスを実装しただけの抽象クラス。相変わらず曖昧模糊。
AbustractSerializableModel.cs
using System; namespace ClassLibrary1 { [Serializable] public abstract class AbustractSerializableModel : ISerializableModel { public abstract Type BindType { get; } public abstract override string ToString(); } }
Base64でシリアル化可能なモデルのカスタムモデルバインダを実装します。下記サンプルのように、自身の型のModelMetadataから、ModelValidatorを取得して自身の型のバリデーションの処理も行うように実装しておくと、カスタムモデルバインダでもアノテーション検証がされるようになり、ViewModelに入れ子となっている場合でも検証を有効にするよう実装することもできます。これは、今回の実装にかかわらず様々な実装で使える方法なので覚えておいて損はないでしょう。
SerializeableModelBinder{T}.cs
using System.Web.Mvc; namespace ModelBinderSample.Models.ModelBinder.Binder { public class SerializeableModelBinder<T> : DefaultModelBinder { public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { if (bindingContext.ModelMetadata.ModelType != typeof(T)) return base.BindModel(controllerContext, bindingContext); var serializedModel = controllerContext.HttpContext.Request[bindingContext.ModelName]; var model = Serializer.Deserialize(serializedModel); ModelMetadata modelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType()); ModelValidator compositeValidator = ModelValidator.GetModelValidator(modelMetadata, controllerContext); foreach (ModelValidationResult result in compositeValidator.Validate(null)) bindingContext.ModelState.AddModelError(bindingContext.ModelName + "." + result.MemberName, result.Message); return model; } } }
Base64のシリアライズとデシリアライズ
Serializer.cs
using System; using System.IO; using System.Runtime.Serialization.Formatters.Binary; namespace ModelBinderSample { public static class Serializer { public static string Serialize(object obj) { using (MemoryStream stream = new MemoryStream()) { var bf = new BinaryFormatter(); bf.Serialize(stream, obj); return Convert.ToBase64String(stream.GetBuffer()); } } public static object Deserialize(string subject) { using (var stream = new MemoryStream(Convert.FromBase64String(subject))) { var bf = new BinaryFormatter(); return bf.Deserialize(stream); } } } }
Sample1.cs
using System; using System.ComponentModel.DataAnnotations; using System.Diagnostics.Contracts; using ClassLibrary1; using ModelBinderSample.Models.ViewModel; namespace ModelBinderSample.Models { [Serializable] public class Sample1 : AbustractSerializableModel { public override Type BindType { get { return this.GetType(); } } [Display(Name="ただのプロパティ")] public string NomalProperty { get; set; } public string[] ParamString { get; set; } public int[] ParamInt { get; set; } public Hoge Hoge { get; set; } public override string ToString() { Contract.Ensures(!string.IsNullOrWhiteSpace(Contract.Result<string>())); return Serializer.Serialize(this); } } }
Sample2.cs
using System; using System.ComponentModel.DataAnnotations; using System.Diagnostics.Contracts; using ClassLibrary1; using ModelBinderSample.Models.ViewModel; namespace ModelBinderSample.Models { [Serializable] public class Sample2 : AbustractSerializableModel { public override Type BindType { get { return this.GetType(); } } [Display(Name = "必須なプロパティ")] [Required(ErrorMessage = "「{0}」は、必須だってばよ!")] public string RequiredProperty { get; set; } public string[] ParamString { get; set; } public int[] ParamInt { get; set; } public Hoge Hoge { get; set; } public override string ToString() { Contract.Ensures(!string.IsNullOrWhiteSpace(Contract.Result<string>())); return Serializer.Serialize(this); } } }
Sample3.cs
using System.ComponentModel.DataAnnotations; using ModelBinderSample.Models.ViewModel; namespace ModelBinderSample.Models { public class Sample3 { [Display(Name = "入力必須なやつ")] [Required(ErrorMessage = "「{0}」は、必須だってばよ!")] public string RequiredProperty { get; set; } public string[] ParamString { get; set; } public int[] ParamInt { get; set; } public Hoge Hoge { get; set; } } }
モデルバインダの登録
protected void Application_Start() { AreaRegistration.RegisterAllAreas(); // Add ModelBinder ModelBinders.Binders.Add(typeof(Sample1), new SerializeableModelBinder<Sample1>()); ModelBinders.Binders.Add(typeof(Sample2), new SerializeableModelBinder<Sample2>()); ModelBinders.Binders.Add(typeof(Sample3), new SerializeableModelBinder<Sample3>()); RegisterGlobalFilters(GlobalFilters.Filters); RegisterRoutes(RouteTable.Routes); }
Sample3クラスは、SerializableでもなければISerializableModelインターフェイスも実装していないので、SerializeableModelBinder
さて、実装サンプルSerializeableModelBinder
拡張ポイントの見極めって一言でいっても、ユーザビリティ的な意味での設計の視点と、開発テクニック的な意味での設計の視点の両方がありますよね
そこで、MEF(Managed Extensibility Framework)を用いて自動拡張型カスタムモデルバインダプロバイダーを作ることを考えてみます。
ExportProviderの拡張 : 任意のインターフェイスの実装をコントラクトとするカスタムエクスポートプロバイダー
さっそく「MEFを用いた自動拡張型カスタムモデルバインダプロバイダー」の作成と行きたいところなんですが、その前に下準備が必要となります。ISerializableModelインターフェイスを実装している具象クラスをコントラクトとするMEFエクスポートが必要になるからです。そのために、任意のインターフェイスの実装をコントラクトとするカスタムエクスポートプロバイダーを作成する必要があります。前回のエントリーではWindows AzureでBlobストレージからMEFのパーツを検索できるカスタムCatalogを紹介しました。今回は、Catalogに比べて、よりピンポイントな条件でエクスポートができる、カスタムエクスポートプロバイダーを紹介します。
任意のインターフェースを指定してExportできるExportProvidorとか、普通にあったら便利っぽいので、誰か実装してくれていないものかな?と思ってちょっと調べたけど、そもそもMEFが流行っていないので期待した俺がバカだったよということで自分で書くしかなかった。
2012-02-14 01:10:27 via web
MEFの入門記事はわかりやすいものがいくつかありますが、入門よりももう少し踏み込んだ情報はあまりありません。海外記事を含めてもカスタムカタログやカスタムエクスポートプロバイダー等の解説記事や簡単なサンプルは決して多くはありません。MEF(Managed Extensibility Framework)を積極的に使おうと考えた場合、カタログやエクスポートプロバイダーのカスタマイズは必須です。オブジェクト指向なスタイルの開発においては、インターフェイスによる多態は日常茶飯事ですし、任意のインターフェイスの実装をコントラクトとするエクスポートプロバイダーとか、欲しくなるのは自然な流れです。ということで、シンプルなサンプルコードを以下に示します。
InterfaceExportProvider{T}.cs
using System; using System.Collections.Generic; using System.ComponentModel.Composition; using System.ComponentModel.Composition.Hosting; using System.ComponentModel.Composition.Primitives; using System.Diagnostics.Contracts; using System.Linq; using System.Reflection; using ClassLibrary1; namespace ClassLibrary2 { public class InterfaceExportProvider<T> : ExportProvider { private readonly IList<InterfaceExportDefinition> exportDefinitions = new List<InterfaceExportDefinition>(); public InterfaceExportProvider() : this(() => Assembly.GetExecutingAssembly().GetTypes(), t => true) { } public InterfaceExportProvider(Func<Type, bool> predicate) : this(() => Assembly.GetExecutingAssembly().GetTypes(), predicate) { Contract.Requires(predicate != null); } public InterfaceExportProvider(Func<Type[]> factory, Func<Type, bool> predicate) { Contract.Requires(factory != null); var types = factory() .Where(t => !t.IsAbstract) .Where(t => !t.IsInterface) .Where(t => predicate(t)); ComposeTypes(types); } protected override IEnumerable<Export> GetExportsCore(ImportDefinition definition, AtomicComposition atomicComposition) { Contract.Ensures(0 <= this.exportDefinitions.Count); return exportDefinitions.Where(ed => definition.ContractName == ed.ContractName) .Select(ed => new Export(ed, () => Util.New(ed.ServiceType))); } [ContractInvariantMethod] private void ObjectInvariant() { Contract.Invariant(typeof(T).IsInterface); } private void ComposeTypes(IEnumerable<Type> serviceTypes) { Contract.Requires(serviceTypes != null); serviceTypes .Where(x => !x.IsAbstract) .Select(type => new { Type = type, InterfaceType = type.GetInterfaces().Where(t => t == typeof(T)).SingleOrDefault()}) .Where (x => x.InterfaceType != null).ToList() .ForEach(x => { var metadata = new Dictionary<string, object>(); metadata[CompositionConstants.ExportTypeIdentityMetadataName] = AttributedModelServices.GetTypeIdentity(x.Type); var contractName = AttributedModelServices.GetContractName(x.InterfaceType); var exportDefinition = new InterfaceExportDefinition(contractName, metadata, x.Type); exportDefinitions.Add(exportDefinition); }); } } }
例えば上記のクラスをデフォルトコンストラクタでインスタンス化した場合、現在実行中のコードを格納しているアセンブリ内のうち、ジェネリックタイプTで指定したインターフェイスをコントラクトとする型についてエクスポートを行います。そういうExportプロバイダー実装です。要するに、ジェネリックタイプTで指定したインターフェイスを実装している具象クラスを検索してオブジェクトグラフのファクトリを行うようなプロバイダーということです。これがあると、オブジェクト指向プログラミングで当たり前のインターフェイスによる多態をひとまとめに"[ImportMany(typeof(インターフェイス))]"というように、Exportできるので嬉しいというわけです。
上記InterfaceExportProvider
InterfaceExportDefinition.cs
using System; using System.Collections.Generic; using System.ComponentModel.Composition.Primitives; using System.Diagnostics.Contracts; namespace ClassLibrary2 { public class InterfaceExportDefinition : ExportDefinition { private readonly string _contractName; private readonly Dictionary<string, object> _metaData; public InterfaceExportDefinition(string contractName, Dictionary<string, object> metaData, Type type) { Contract.Requires(metaData != null); Contract.Requires(type != null); Contract.Ensures(this._contractName == contractName); Contract.Ensures(this._metaData == metaData); this._contractName = contractName; this._metaData = metaData; ServiceType = type; } public Type ServiceType { get; private set; } [ContractInvariantMethod] private void ObjectInvariant() { Contract.Invariant(this._metaData != null); } public override IDictionary<string, object> Metadata { get { Contract.Ensures(this._metaData != null); Contract.Ensures(Contract.Result<IDictionary<string, object>>() == this._metaData); return this._metaData; } } public override string ContractName { get { Contract.Ensures(Contract.Result<string>() == this._contractName); return this._contractName; } } } }
これで、任意のインターフェイスの実装をコントラクトとするカスタムエクスポートプロバイダーができました。オブジェクト指向においては、インターフェイスによる多態は日常茶飯事ですので利用場面はたくさんありそうですね。
MEFを用いた自動拡張型カスタムモデルバインダプロバイダー
では作成したInterfaceExportProvider
SerializeableModelBinderProvider.cs
using System; using System.Collections.Generic; using System.ComponentModel.Composition; using System.ComponentModel.Composition.Hosting; using System.Linq; using System.Web.Mvc; using ClassLibrary1; using ClassLibrary2; using ModelBinderSample.Models.ModelBinder; using System.Collections.Concurrent; namespace ModelBinderSample.Models.ModelBinderProvider { public class SerializeableModelBinderProvider : IModelBinderProvider, IDisposable { private bool disposed; private readonly ConcurrentDictionary<Type, Type> _cache = new ConcurrentDictionary<Type, Type>(); [ImportMany(typeof(ISerializableModel))] private IEnumerable<Lazy<ISerializableModel>> _serializableModels = null; private CompositionContainer _Container = null; private SerializeableModelBinderProvider() { this.disposed = false; } public SerializeableModelBinderProvider(Func<Type[]> factory) : this() { ComposeParts(factory); } public IModelBinder GetBinder(Type modelType) { this.ThrowExceptionIfDisposed(); if (CanBind(modelType)) { var modelBinderType = _cache.GetOrAdd(modelType, typeof(SerializeableModelBinder<>).MakeGenericType(modelType)); return (IModelBinder)Activator.CreateInstance(modelBinderType); } return null; } public bool CanBind(Type modelType) { if (_cache.ContainsKey(modelType)) return true; var count = _serializableModels.Where(m => m.Value.BindType == modelType).Count(); if (count > 0) return true; return false; } protected void ThrowExceptionIfDisposed() { if (this.disposed) { throw new ObjectDisposedException(this.GetType().ToString()); } } public void ComposeParts(Func<Type[]> factory) { this.ThrowExceptionIfDisposed(); var provider = new InterfaceExportProvider<ISerializableModel>(factory, x => x.IsSerializable); _Container = new CompositionContainer(provider); _Container.ComposeParts(this); } protected virtual void Dispose(bool disposing) { lock (this) { if (this.disposed) { return; } this.disposed = true; if (disposing) { if (_Container != null) { _Container.Dispose(); _Container = null; } } } } public void Dispose() { this.Dispose(true); GC.SuppressFinalize(this); } } }
このような汎用的なカスタムモデルバインダプロバイダーを作成することで、Sample4, Samole5...と、シリアル化可能なクラスを次々と定義していくだけで、自動的に拡張されていくカスタムエクスポートプロバイダーを作成することができるというわけです。MEFはユーザーの目に見えるような機能面での拡張のみならず、開発視点においても確実に設計の幅を広げてくれます。MEFは.NET Framework4標準ですので、臆することなくガンガン使っていけるのがうれしいですね。
IModelBinderProviderインターフェイスがイケてない説
まず、System.Web.Mvc.IModelBinderProviderインターフェイスの定義をご覧いただきましょう。
public interface IModelBinderProvider { IModelBinder GetBinder(Type modelType); }
モデルの型を引数で受け取り、適切なモデルバインダを返すだけのGetBinderメソッドを持つ、とてもシンプルなインターフェイスです。あまりにもシンプルすぎて、モデルバインダプロバイダーがどんなモデルの型を対象としたプロバイダーなのか外部から知るすべもありません。GetBinderメソッドの戻り値が null だったら、次のモデルバインダプロバイダーに処理を委譲する作りになっているので、複数のカスタムモデルバインダプロバイダーが協調して動作するようにするには、サポートしないモデルの型の場合に必ず null を返さなければなりません。「該当する結果がない場合にnullを返して、戻り値側でそれがnullだったら次の処理を...」という仕様はあんましイクナイ(・Α・)と思います。もっと別の方法もあっただろうに...。
あと、おまけ。
Util.cs
using System; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Web.Mvc; namespace ClassLibrary1 { public static class Util { public static T New<T>() { Type type = typeof(T); Func<T> method = Expression.Lambda<Func<T>>(Expression.Block(type, new Expression[] { Expression.New(type) })).Compile(); return method(); } public static object New(Type type) { Func<object> method = Expression.Lambda<Func<object>>(Expression.Block(type, new Expression[] { Expression.New(type) })).Compile(); return method(); } public delegate TInstance ObjectActivator<TInstance>(params object[] args); public static ObjectActivator<TInstance> GetActivator<TInstance>(ConstructorInfo ctor) { Type type = ctor.DeclaringType; ParameterInfo[] paramsInfo = ctor.GetParameters(); ParameterExpression param = Expression.Parameter(typeof(object[]), "args"); Expression[] argsExp = new Expression[paramsInfo.Length]; for (int i = 0; i < paramsInfo.Length; i++) { Expression index = Expression.Constant(i); Type paramType = paramsInfo[i].ParameterType; Expression paramAccessorExp = Expression.ArrayIndex(param, index); Expression paramCastExp = Expression.Convert(paramAccessorExp, paramType); argsExp[i] = paramCastExp; } NewExpression newExp = Expression.New(ctor, argsExp); LambdaExpression lambda = Expression.Lambda(typeof(ObjectActivator<TInstance>), newExp, param); ObjectActivator<TInstance> compiled = (ObjectActivator<TInstance>)lambda.Compile(); return compiled; } } }
モデルバインダプロバイダーの登録
protected void Application_Start() { AreaRegistration.RegisterAllAreas(); // Add ModelBinderProvider ModelBinderProviders.BinderProviders.Add(new SampleViewModel0BinderProvider()); ModelBinderProviders.BinderProviders.Add(new SerializeableModelBinderProvider(() => Assembly.GetExecutingAssembly().GetTypes())); RegisterGlobalFilters(GlobalFilters.Filters); RegisterRoutes(RouteTable.Routes); }
さてコード中心の記事でしたが、ASP.NET MVC3のカスタムモデルバインダとカスタムモデルバインダプロバイダーについてのサンプルプログラムと、MEFのカスタムエクスポートプロバイダーを利用した自動拡張型のコンポーネント設計の手法について見てきました。モデルバインダの仕組みはASP.NET MVC3のコアコンポーネントのひとつであり基本中の基本ですので、既定のDefaultModelBinderのみに頼るのではなく、このあたりの仕組みや拡張・設計ポイントはしっかり押さえておきたいところです。長々と書きましたが、何かの参考になれば幸いです。
F#はちょい充電中。
Windows AzureとMEFで再デプロイを必要としない拡張(または縮小)可能なクラウドアプリケーション。BlobストレージからMEFのパーツを検索できるカスタムカタログ BlobStorageCatalog を作ろう。
しばらくF#ネタしか書いていませんでした。たまにはC#ネタを置いておきます。
C#ネタというよりは、最近仕事で利用しているWindows AzureとMEFのネタですが。
Managed Extensibility Framework (MEF)とは
Managed Extensibility Framework (MEF) は、.NET Frameworkに最適な拡張可能なアプリケーションを作成するための一連の機能を提供する軽量ライブラリです。既定で提供されているのは属性プログラミングモデルですが、MEFのコアAPIは全く属性に依存していないので、MEFのコアAPIにアクセスする方法を定義したクラスを独自に作成することでリフレクションベースのプログラミングモデルなど、様々なスタイルで利用可能です。
MEFは、.NET Framework 4 およびSilverlight 4で標準利用可能です。なお現在もオープンソースで開発が進められていて、ASP.NET MVC向けのComposition providerなどを含む、MEF 2 Preview 5がCodePlexよりダウンロード可能です。wktkですね。
「MEFとは、簡単に言うとDIである」というような説明がされがちですが、一般的なIoCコンテナ(DIコンテナ)フレームワークほど多機能ではありません。MEFは、 IoCコンテナ(DIコンテナ)で言うところの、いわゆるオブジェクトグラフのファクトリをメインとして機能します。つまり、実行時に解決する必要があるクラスのメンバーを動的に認識して処理することができます。しかし逆に言うと、一般的なIoCコンテナ(DIコンテナ)フレームワークほど豊富な機能はなく、非常にコンパクトな構成です。MEFにもキャッシュ機能があるのは確かですが、インスタンスのキャッシュを最小限しかサポートしません。また、.NET Framework 4 に同梱されているバージョンのMEF では処理のインターセプト(AOPサポート)の機能がまったくありません。純粋に、IoCコンテナ(DIコンテナ)フレームワークのさまざまな機能を求める場合、MEFでは満足できないでしょう。
では、どのような場合にMEFを利用すれば良いのか。それは、アプリケーションが汎用性のある機能拡張(プラグイン,アドイン)を求めているかどうかです。これに該当する場合はMEFの利用が有効で、そうではない場合は、あえてMEFを利用する意味はあまりないでしょう。
拡張(または縮小)可能なクラウドアプリケーション
「拡張(または縮小)可能なクラウドアプリケーション」などと大風呂敷を広げてみたものの、わたしはWindows AzureのWebRoleとWorkerRoleくらいしかかじったことがありません。ここではWindows Azureにおいて、再デプロイを必要としない拡張可能なWebRole、WorkerRoleを作りたいケースについて考えます。.NET Frameworkで拡張可能なアプリケーションを作りたい場合、有効な方法としてMEFがあると前述しました。Azureに置いてもそれは同じです。しかし、Azureで一度デプロイしたものについて動的に機能拡張をするにはどのようにしたらよいのでしょう。まっさきに思い浮かぶのが、Blobストレージの利用です。Blobストレージに格納してあるアセンブリを、ローカルストレージにダウンロードして、DirectoryCatalogクラスを利用してパーツをExportする方法が考えられます。しかしその方法では、対象のアセンブリについてプロセスが掴んでしまうため、デプロイ済みのサービスについて、動的に動作を拡張 or 変更するということができません。ではどうするか。その場合、ローカルストレージは利用せず、BlobストレージからダウンロードしたアセンブリをAssemblyCatalogクラスを利用して直接パーツをExprotする方法を取るとうまくいきます。でも、これをいちいちプログラミングするのは非常に面倒くさいです。常識的に考えて部品化ですね。Blobストレージ内のアセンブリからMEFのパーツを検索できる専用のカタログクラスであるところの、BlobStorageCatalogとか作っちゃえばいいと思います。
以下、実装サンプルです。
BlobStorageCatalog.cs
using System; using System.Collections.Generic; using System.ComponentModel.Composition.Hosting; using System.ComponentModel.Composition.Primitives; using System.Diagnostics.Contracts; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Threading; using Microsoft.WindowsAzure; using Microsoft.WindowsAzure.StorageClient; namespace ClassLibrary2 { public class BlobStorageCatalog : ComposablePartCatalog, ICompositionElement { private readonly object _thisLock = new object(); private readonly CloudStorageAccount _account = null; private readonly static Dictionary<string, byte[]> _dicAssembly = new Dictionary<string, byte[]>(); public string ContainerName { get; private set; } public string BlobName { get; private set; } private AssemblyCatalog _innerCatalog = null; private int _isDisposed = 0; protected BlobStorageCatalog() {} public BlobStorageCatalog(CloudStorageAccount account, string containerName, string blobName) : this() { Contract.Requires(account != null); Contract.Requires(!String.IsNullOrWhiteSpace(containerName)); Contract.Requires(!String.IsNullOrWhiteSpace(blobName)); Contract.Requires(containerName == containerName.ToLower()); this._account = account; this.ContainerName = containerName; this.BlobName = blobName; } private ComposablePartCatalog InnerCatalog { get { this.ThrowIfDisposed(); lock (this._thisLock) { if (_innerCatalog == null) { var catalog = new AssemblyCatalog(LoadAssembly(this._account, this.ContainerName, this.BlobName)); Thread.MemoryBarrier(); this._innerCatalog = catalog; } } return _innerCatalog; } } public override IEnumerable<Tuple<ComposablePartDefinition, ExportDefinition>> GetExports(ImportDefinition definition) { return this.InnerCatalog.GetExports(definition); } public override IQueryable<ComposablePartDefinition> Parts { get { return this.InnerCatalog.Parts; } } private string GetDisplayName() { return string.Format(CultureInfo.CurrentCulture, "{0} (BlobStorage: ContainerName=\"{1}\", BlobName=\"{2}\") (Assembly=\"{3}\")", GetType().Name, this.ContainerName, this.BlobName, _innerCatalog.Assembly.FullName); } public override string ToString() { return GetDisplayName(); } private void ThrowIfDisposed() { if (this._isDisposed == 1) { if (this == null) throw new NullReferenceException(this.GetType().Name); throw new ObjectDisposedException(this.GetType().ToString()); } } protected override void Dispose(bool disposing) { try { if (Interlocked.CompareExchange(ref this._isDisposed, 1, 0) == 0) { if (disposing) { if (this._innerCatalog != null) { this._innerCatalog.Dispose(); } } } } finally { base.Dispose(disposing); } } string ICompositionElement.DisplayName { get { return GetDisplayName(); } } ICompositionElement ICompositionElement.Origin { get { return null; } } private static Assembly LoadAssembly(CloudStorageAccount account, string containerName, string blobname) { var blobStorage = account.CreateCloudBlobClient(); var container = blobStorage.GetContainerReference(containerName); var blob = container.GetBlobReference(blobname); var blobUri = container.Uri + "/" + blobname; using (var strm = new MemoryStream()) { blob.DownloadToStream(strm); byte[] asseblyBytes = strm.ToArray(); if (!_dicAssembly.ContainsKey(blobUri)) { _dicAssembly.Add(blobUri, asseblyBytes); return Assembly.Load(asseblyBytes); } if (Enumerable.SequenceEqual(asseblyBytes, _dicAssembly[blobUri])) { return Assembly.Load(_dicAssembly[blobUri]); } _dicAssembly[blobUri] = asseblyBytes; return Assembly.Load(asseblyBytes); } } } }
だいたいこんな感じですかね。特に問題はないと思いますが、厳密な検証はしていません。実戦投入は計画的に。
拡張(または縮小)可能なWinedow Azure上で動くASP.NET MVC3 Webアプリケーションのサンプル
MEFで拡張可能な計算アプリケーション作成用のインターフェイスを定義
namespace ClassLibrary1 { public interface ICalculator { IEnumerable<char> Symbols { get; } string Calculate(int left, int right, char operation); } public interface IOperation { int Operate(int left, int right); } public interface IOperationData { Char Symbol { get; } } }
足し算と引き算を定義
namespace Calculator { [Export(typeof(IOperation))] [ExportMetadata("Symbol", '-')] class Subtract : IOperation { public int Operate(int left, int right) { return left - right; } } [Export(typeof(IOperation))] [ExportMetadata("Symbol", '+')] class Add : IOperation { public int Operate(int left, int right) { return left + right; } } }
掛け算を新たに定義してビルドし、Calculator.dllを作る
[Export(typeof(IOperation))] [ExportMetadata("Symbol", '*')] public class Multiply : IOperation { public int Operate(int left, int right) { return left * right; } }
ページを再度読み込みなおすと、拡張した掛け算部分が追加されて利用可能に。
という具合に、MEFを利用することで、再度デプロイしなおさなくても、拡張(または縮小)可能なWindows Azureアプリケーションを設計することができます。
このように、MEF は .NET Framework の拡張(または縮小)可能なアプリケーションを構築する際にとても便利なソリューションですが、このとき最も難しい部分は、拡張できるようにアプリケーションを設計することです。これは拡張可能アプリケーション設計そのもの難しさであって、MEFテクノロジそのものが難しいわけではありません。上記のサンプルでは拡張性の乏しい単純な構造の設計となっていますが、より汎用的な拡張が必要なアプリケーションを設計する場合、一気に複雑になります。MEFで提供されているAPIを十分理解した上で、適切な拡張ポイントを見つけ出して、それを将来を見据えたかたちでどれだけ汎用的に拡張できるよう設計できるかがポイントとなってきます。このような設計では、オブジェクト指向の考え方が重要になるでしょう。
ここでは省略しますが、もちろんF#でも同様のアプリケーションを書くことが可能です。
ただF#でMEFを利用する場合はちょっと癖があるんですけどね。でもそれはまた別のお話。
拡張可能なWinedow Azure上で動くASP.NET MVC3 WebアプリケーションのサンプルプログラムをSkyDriveにアップしておきます。
SkyDrive - Azure+MEF.zip
独習ASP.NET第3版を頂きました。
3月26日に参加した第56回CLR/H勉強会にて、WINGSプロジェクトさんより「独習ASP.NET第3版(翔泳社)」を頂きました。
WINGSプロジェクトさん、翔泳社さん、id:naoki0311さん*1、CLR/Hスタッフの皆さん、どうもありがとうございます。
- 作者: 山田祥寛
- 出版社/メーカー: 翔泳社
- 発売日: 2011/02/16
- メディア: 大型本
- 購入: 11人 クリック: 107回
- この商品を含むブログ (10件) を見る
わたしはこれで再入門しました
その名の通り、ASP.NETの基礎をひととおり独習できる内容となっています。
ASP.NETの特徴や仕組みについて丁寧に解説してあり、理解度チェックとして確認用の練習問題も付いています*2。
世間の流れはASP.NET MVCとなりつつありますが、かと言って昔ながらのASP.NET開発の知識が不要というわけではないので、
技術的な背景をしっかり押さえつつ網羅的に学習できる書籍の存在意義は大きいと思います。
こちらの書籍はサンプルコードも豊富(配布ページよりダウンロードすることができる。VB.NETとC#有り)なので、
VB.NETあるいはC#の経験さえあれば、スムーズかつ効果的にASP.NETについて学習することができます。
ASP.NETをはじめてみたい方はこの書籍で入門してみてはいかがでしょうか。わたしはこれで再入門しました。
個人的によかったところ
・第2章 ASP.NETの基礎
基本的なASP.NETの仕組みを学習できた。いままで「なんとなく」だった部分が明確になった。
・第7章 状態管理
HTTPはステートレスなプロトコルなので、ASP.NETでは複数ページ/リクエスト間での情報維持の方法を熟知している必要がある。
局面に応じて、どんな状態管理機能を利用するのが良いかの判断基準を得られた。
・第10章 部品化技術
「部品化」は、ASP.NETに限らずアプリケーション開発では基本的なことで且つ大事なところ。
HTTPハンドラーとかPage派生クラスとか。ASP.NET3.5との互換性についても触れられている。参考にしたい。
・第11章 Ajax開発
JavaScriptを用いてAjaxなWebアプリは書いたことがあったが、ASP.NET AJAXは使ったことがなかったので勉強になった。
・付録A IISへの配置
実際に開発したWebアプリをIIS(7.5)へ配置する方法について解説がある。初心者にやさしい。
おまけ:F#でもASP.NET開発
「独習ASP.NET第3版」 P14より引用
C++、F#はASP.NETの開発には利用できません。一般的に、ASP.NETの開発にはVisual Basic、またはC#を使用します。
一般的にはC#あるいはVB.NETを利用します。はいその通りです。
その通りなのですが、「F#でASP.NET開発」はできないこともないです(C++は知らない)。
id:fitsさん
F# で ASP.NET - なんとなくな Developer のメモ
http://d.hatena.ne.jp/fits/20100906/1283766723
id:bleis-tiftさん
F# でブログアプリを作る (Very Easy 編) - 予定は未定Blog版
http://d.hatena.ne.jp/bleis-tift/20101211/1292074867
F#でのASP.NET開発は、「できなくもないが、あえてする理由もない」というのが現状です。
あえて茨の道を行く必要はまったくありませんが、F#は“できる子”です。
「HTML5基礎」を頂きました
先日参加した第57回CLR/H勉強会にて、「HTML5基礎( 毎日コミュニケーションズ)」を頂きました。
WINGSプロジェクトさん、どうもありがとうございます。こちらも読み次第書評を書かせていただきます。
- 作者: WINGSプロジェクト片渕彼富,山田祥寛
- 出版社/メーカー: 毎日コミュニケーションズ
- 発売日: 2011/03/26
- メディア: 単行本(ソフトカバー)
- クリック: 87回
- この商品を含むブログ (11件) を見る