de:codeも終わったので、勢いで Xamarin Tシャツアプリを F# に書き直してます。Xamarin Tシャツアプリってのは、http://xamarin.com/studio から Xamarin Studio をダウンロードして、ダウンロードしたソースコードをビルドすると Tシャツを注文するアプリが作れます。それを実際に送信すると、Xamarin 社が C# の Tシャツを送ってくれるという販促アプリです。
で、じゃあ、F# で書いたらどうなるのか?ってのと、F# で送ったら F# Tシャツを送ってくれるかどうか…は別として(多分無理でしょう)、F# のアプリが iOS/Android で動くよ、の実践版のつもりです。実際 C# のコードをダウンロードすると、Web Service に接続しているし、リストボックスとか POCO のデータアクセスなどを使っている、iOS のグラフィック機能を使ってごちゃごちゃやっているという結構な代物でした。その中で、サーバーアクセスなどで頻繁に await/async をやっています。
ざっと F# に書き直しながら思ったのは、
- 非同期処理の async/await が頻繁に出てくる
- データバインド絡みの Obeserver パターンが出てくる
この2点を F# でも書けないといけない。非同期処理自体は Windows ストアアプリでも頻繁に出て来るのですが、WinRT へ F# からアクセスできない(?)ので、まだ問題になっていない模様。しかし、Xamarin.iOS/Android を使って F# をアプリを作るときは、C# の async/await は避けられないわけで、このあたりはうまく C# と F# で相互運用したいところです。
Async in C# and F#: Asynchronous gotchas in C# (Japanese translation)
https://gist.github.com/pocketberserker/5565303
Async in C# and F#: Asynchronous gotchas in C#
http://tomasp.net/blog/csharp-async-gotchas.aspx/
これは、話題になった時にざっと読んで、F# と C# で Async の扱いが違うのか、漠然と思っていたわけですが、いやいやいや、Async が違うというんじゃなくて、そもそもこの話題で出てくる F# の Async はコンピュテーション式の AsyncBuilder で、C# の async/await は Awaitable パターンの実装だから、そもそもが違うがな(と自分が勘違い)していたことが、今日わかりました。なるほど。そうなると async/await を Async{} で単純に置き換えることができないわけで、更に言えば、.NET Framework の *Async メソッドの扱いと、F# の Async{} の扱いを両方やらないと駄目ってことですね。Task<‘T> と Async<‘A> ってことです。
■Async は Async<‘T> を返す
let workThenWait() = async { Thread.Sleep(1000) printfn "work done" do! Async.Sleep(1000) } let demo() = let work = workThenWait() |> Async.StartAsTask printfn "started" work.Wait() printfn "completed"
workThenWait() が非同期で動いている間、待っているという書き方です。
Async.StartAsTask では、まだ実行されていなくて(タスクだけ作られて)、work.Wait() でタスクが実行されます。work の型は、Async<‘T> で、終了時に戻り値を返せます。
これを、素直に C# に直すと、こんな感じ。
async Task WorkThenWait() { Thread.Sleep(1000); Console.WriteLine("work"); await Task.Delay(1000); } async void Demo() { Console.WriteLine("started"); await WorkThenWait(); Console.WriteLine("completed"); }
WorkThenWait() の位置が、started の後ろにずれて、await で非同期待ちをします。正確には非同期実行&待ちをするわけで、元ネタでいう、var child = WorkThenWait(); と child.Wait(); にあたります。
これから勝手に推測して、Async{} ってのを async/await に直せばよいか?って思うわけですが違っていて、C# の async/await のほうは Task<‘T> を返します。
■async/await は Task<‘T> を返す
下記の例では Task を返すように書き方もの。
C# では、async で修飾すれば、Task を返すのにラムダ式を使わなくてよいし、非同期待ちをするのに、Start(), Wait() を羅列しなくてもよい。更に戻り値を受けるときも、t.Result を使う必要もなく await が使える。
let workThenWait2() = new Task( fun _ -> Thread.Sleep(1000) printfn "work done" Thread.Sleep(1000) ) let demo2() = let t = workThenWait2() printfn "started" t.Start() t.Wait() printfn "completed"
int 型の戻り値を返したときは、こんな風に書く。
let workThenWait3() = new Task( fun _ -> Thread.Sleep(1000) printfn "work done" Thread.Sleep(1000) 1 // 戻り値 ) let demo3() = let t = workThenWait3() printfn "started" t.Start() t.Wait() let res = t.Result printfn "completed"
■何が問題か?
F# だけでやっているときは、Async{} を使えばいいのだけど、C# と相互運用したいときとか、非同期の .NET Framework クラスを使いたいとき(WebClientとか)に結構困る。Async と async/await が混在するのもそうだけど、Async<‘T> なのか、Task<‘T> なのかを区別しないといけない。
- WebClient で使う Async 系のメソッドは、Task<‘T> を返すので、これに合わせる。
- C# で作った Async 系のメソッドは、Task<‘T> を返す。
- C# へ渡す非同期タスクは Task で渡す必要がある。
- F# 内で閉じているときは Async{} を使う
というパターンが必要になる。
■F# 用の await を自作する
まず Task<‘T> を受けるための await を自作します。
type Async() = // await を自作する static member await (t:Task) = t.Start() t.Wait() static member await (t:Task<'a>) = t.Start() t.Wait() t.Result
一度、Await で包んであるのは、await メソッドを多重化するため。戻り値無しだけ作るという方法もありでしょう。
使い方は、C# の await とは違って、後ろにつけます。Async.StartAsTask と同じ方法ですね。
let demo2a() = printfn "started" workThenWait2() |> Async.await printfn "completed" let demo3a() = printfn "started" let res = workThenWait3() |> Async.await printfn "completed"
同じように、Async<‘T> を引数に取る await メソッドも作っておきます。
type Async() = // await を自作する static member await (t:Task) = t.Start() t.Wait() static member await (t:Task<'a>) = t.Start() t.Wait() t.Result static member await (t:Async<'a>) = ( t |> Async.StartAsTask ).Wait()
こうすると、どちらの場合も Async.await で処理して統一できるので便利。
let demo1a() = printfn "started" workThenWait() |> Async.await printfn "completed"
■Task を作る TaskBuilder を作ればよい
C# の Task<‘T> は楽に処理できるようになったので、今度は F# で Task<‘T> を作って C# に渡す、というパターンを考える。async の task 版といったところ。
let workThenWait3() = new Task( fun _ -> Thread.Sleep(1000) printfn "work done" Thread.Sleep(1000) 1 // 戻り値 )
このようなラムダ式の書き方を、以下なようなコンピュテーション式に直せればOK。これは AsyncBuilder のソースを見て直せばいいかな?
let workThenWait3() = task { Thread.Sleep(1000) printfn "work done" Thread.Sleep(1000) 1 // 戻り値 }
こっちは後日。
~~~
こちらも参照
AsyncBuilder extension for maniplating other containers by using keyword | F# Snippets
http://www.fssnip.net/jj
自分はC#側ではTaskで考えて、F#側ではAsyncでやっとるです(´・ω・`)
F#のAsyncをC#側で使う場合は
let startAsTask asyncF=
let tcs=new TaskCompletionSource()
async{try let! r=asyncF
tcs.SetResult r
with ex->tcs.SetException ex
}|>Async.Start
tcs.Task
※ここ、Async.StartAsTask知らんで自作しとった( ゚∀゚)・∵. グハッ!!
static public Task StartAsTask(this FSharpAsync fsAsync)
{
return Model.Helper.startAsTask(fsAsync);
}
この辺でTask化、
Taskを返すものをF#側で使うときは
async{return! Async.AwaitTask<|readStrFromFileFunc.Invoke(path,fName)}
※readStrFromFileFuncはC#側からインジェクトされたもの
C# に Task を返すところはまだだったので参考になります。
なるほど、async { return! } を使って返せば OK か。
let hc = new HttpClient()
let res = hc.GetStringAsync(App.Model.Uri.ToString() + “fwd”).Result
_result.Text <- res と Task<'a> は .Result で、そのまま返せる模様。TaskAwait とかなくても動いている。後で詳細にチェックしておく。