かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

非同期ワークフローとメッセンジャーのコールバック

ちょっとした小ネタです。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のRaiseメソッドにコールバックを渡すことで、Viewの処理結果を受け取って続きの処理を行うことが出来ます。コールバックは、ネストが深くなればなるほど嫌な感じになるので、出来れば避けたいところです。因みに実際に画面を用意するのはめんどくさいのでコンソールアプリケーションで作ったコールバックバージョンのコードを以下に示します。

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を返すようにしています。このようにしておくと、以下のように非同期ワークフロー内でRaiseメソッドが呼び出せます。

// 非同期ワークフローを使うとコールバックが消えてすっきり
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#で書くことを真面目に考えてもいいかも?