ちょっとした小ネタです。F#には非同期ワークフローという非同期処理を同期的に書けるという素敵な機能があります。
MSDNの非同期ワークフローのページからコードを拝借しますが、以下のようなコードが書けます。
open System.Net open Microsoft.FSharp.Control.WebExtensions let urlList = [ "Microsoft.com", "http://www.microsoft.com/" "MSDN", "http://msdn.microsoft.com/" "Bing", "http://www.bing.com" ] let fetchAsync(name, url:string) = async { try let uri = new System.Uri(url) let webClient = new WebClient() let! html = webClient.AsyncDownloadString(uri) printfn "Read %d characters for %s" html.Length name with | ex -> printfn "%s" (ex.Message); } let runAll() = urlList |> Seq.map fetchAsync |> Async.Parallel |> Async.RunSynchronously |> ignore runAll()
非同期にURLのデータを読み取って出力するという処理です。非同期の処理をしてるのに同期的な処理のコードと同じように書けるのが特徴です。素敵!!ということで、これを使うとMVVM的にどんなおいしいことがあるのかというと・・・ViewModelからViewを操作するためのMessengerパターンで何か使えそうです。
MVVMをサポートするライブラリの1つPrismではInteractionRequest
open System open System.Windows open Microsoft.Practices.Prism.Interactivity.InteractionRequest open Microsoft.Practices.Prism.Interactivity let r = InteractionRequest<Confirmation>() // View側ではRaisedイベントを購読して処理をしているので、ちょっとエミュレート。 r.Raised.Add <| fun e -> // メッセージボックスを表示してOKが押されたらConfirmedにtrueを設定する。 let result = MessageBox.Show(string e.Context.Content, e.Context.Title, MessageBoxButton.OKCancel) let confirmation = e.Context :?> Confirmation confirmation.Confirmed <- (result = MessageBoxResult.OK) e.Callback.Invoke() // Raiseした後の続きの処理はコールバックで行う r.Raise( Confirmation(Title = "メッセージ", Content = "お試し"), fun result -> match result.Confirmed with | true -> printfn "OKが押されました" | false -> printfn "Cancelが押されました") Console.ReadKey() |> ignore
このコードを実行するとダイアログが表示されOKを押すと「OKが押されました」Cancelを押すと「Cancelが押されました」と表示されます。コメントにもありますがRaiseメソッドを呼び出しているところでコールバックを指定しているので、ちょっと処理が読みにくくなっています。今回はコールバック内の処理は短いですが、ちょっと長くなったり、別メソッドに切り出したりすると、とたんに処理の流れを追うのが大変になります。
Raiseメソッドの非同期ワークフロー対応
ということで以下のようにInteractionRequest
open System open System.Windows open Microsoft.Practices.Prism.Interactivity.InteractionRequest open Microsoft.Practices.Prism.Interactivity // InteractionRequest<T>のメソッドを拡張 type InteractionRequest<'a when 'a :> Notification> with /// 非同期ワークフロー対応のRaiseメソッド member x.RaiseAsync (n : 'a) = Async.FromContinuations (fun (cont, econt, ccont) -> // Raiseのコールバックでcont x.Raise(n, fun r -> cont(r)) )
RaiseAsyncメソッドを定義して、FromContinuationsメソッドを使ってAsync
// 非同期ワークフローを使うとコールバックが消えてすっきり async { let! result = r.RaiseAsync(Confirmation(Title = "メッセージ", Content = "お試し")) match result.Confirmed with | true -> printfn "OKが押されました" | false -> printfn "Cancelが押されました" } |> Async.Start
コールバックを排除できたので、まるで一連の流の処理のようにコードを書けるようになりました。とても素敵ですね!
非同期ワークフロー対応版のコードの全体を以下に示します。
open System open System.Windows open Microsoft.Practices.Prism.Interactivity.InteractionRequest open Microsoft.Practices.Prism.Interactivity // InteractionRequest<T>のメソッドを拡張 type InteractionRequest<'a when 'a :> Notification> with /// 非同期ワークフロー対応のRaiseメソッド member x.RaiseAsync (n : 'a) = Async.FromContinuations (fun (cont, econt, ccont) -> // Raiseのコールバックでcont x.Raise(n, fun r -> cont(r)) ) let r = InteractionRequest<Confirmation>() // View側ではRaisedイベントを購読して処理をしているので、ちょっとエミュレート。 r.Raised.Add <| fun e -> // メッセージボックスを表示してOKが押されたらConfirmedにtrueを設定する。 let result = MessageBox.Show(string e.Context.Content, e.Context.Title, MessageBoxButton.OKCancel) let confirmation = e.Context :?> Confirmation confirmation.Confirmed <- (result = MessageBoxResult.OK) e.Callback.Invoke() // 非同期ワークフローを使うとコールバックが消えてすっきり async { let! result = r.RaiseAsync(Confirmation(Title = "メッセージ", Content = "お試し")) match result.Confirmed with | true -> printfn "OKが押されました" | false -> printfn "Cancelが押されました" } |> Async.Start Console.ReadKey() |> ignore
これは、ViewModelをF#で書くことを真面目に考えてもいいかも?