ScalaでC#のasync, awaitを実現するライブラリ「async」の紹介
この記事は Iwate Advent Calendar 2014 の22日目の記事です。昨日はnana4gontaさんのFirefox Developer Editionを使って他ブラウザをリモートデバッグする - Qiitaでした。明日はayokuraさんです。
岩手関連の記事を書こうと思い、仕事で使おうとしてるD3.jsを使って岩手県を書こうとしたらズバリそのままのサイトを発見して挫折しました。
岩手県ぬりえ
http://acuerdo.m18u.net/iwate_nurie/
結局思いつかなかったので全然関係ない話題を書きます。
さて、Scalaで非同期処理を扱うにはFuture
とPromise
という仕組みを利用します。(ここでは、標準ライブラリのものを指してます。)
Future
とPromise
は非同期で処理する、まだ存在しない処理結果を扱うための仕組みで、複数の非同期処理結果を扱うためには基本的にコールバック(map
やflatMap
、for式
など)を使用するため、コーディング量が若干多くなります。
一方C#では、非同期処理を扱うための構文としてasync
修飾子とawait
演算子が用意されています。これらを利用することで、非同期処理を同期処理のように書くことができ、コーディングの内容が比較的見やすくなっています。
今回は、C#のasync
とawait
をScalaで実現したライブラリ「async
」を紹介したいと思います。
2つの言語の非同期処理を比較する
今回の記事では以下の非同期処理を書き、実際にどのようなコードになるかを比較してみます。
- 数値を非同期で取得する
getInt
関数 - 非同期で取得した2つの数値を足す
add
関数
Scalaの実装
まずはScalaの実装です。
import scala.concurrent._ import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global def getInt(x: Int): Future[Int] = Future { Thread.sleep(1000) x } def add(x: Future[Int], y: Future[Int]): Future[Int] = { for { a <- x b <- y } yield a + b } println( Await.result(add(getInt(1), getInt(2)), 10.seconds) )
add関数で、非同期で処理される2つの数値を足すという非同期処理をしています。この程度の処理であればすんなり理解できますが、もっと多いFuture
を扱う場合などは、処理結果をブロッキングしないように扱うために気を使ってコードを書く必要があります。(単に私のScalaレベルが低いだけですが・・・)
C#の実装
次に、C#での実装です。
(ここでは、C#の慣習のならってメソッド名をGetIntAsync
とAddAsync
と変えて実装しています。)
using System; using System.Threading; using System.Threading.Tasks; class Program { public static void Main() { var program = new Program(); var x = program.GetIntAsync(1); var y = program.GetIntAsync(2); Console.WriteLine( program.AddAsync(x, y).Result ); } public Task<int> GetIntAsync(int x) { return Task.Run<int>(() => { Thread.Sleep(2000); return x; }); } public async Task<int> AddAsync(Task<int> x, Task<int> y) { return await x + await y; } }
注目していただきたいところはAddAsync
メソッドです。非同期で処理される値を足す処理ですが、非同期処理の結果に対してasync
演算子を追加することで、同期処理と同じように処理を書くことができます。Scalaのadd
関数と見比べるとこちらのほうが見やすいのではないかと思います。
async
演算子は、式を書ける場所にはどこにでも追加できるため、扱う非同期処理が多くなっても基本的に同期処理と同じように書くことが可能です。
Scalaでasync, awaitを実現するライブラリ「async
」について
上で紹介したC#のasync
、await
については、SIP 22でScalaの標準ライブラリに含むかどうかが検討されており、現在は別ライブラリ「async」として提供されています。
scala/async
https://github.com/scala/async
このライブラリを利用することで、Scalaでも同期処理のようにコーディングすることができます。上で書いたScalaの実装を、このライブラリを利用して書きなおしたものが以下のコードです。(上で書いたScalaの実装と区別するために、関数名にAsync
をつけました。)
import scala.concurrent._ import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global import scala.async.Async.{async, await} def getIntAsync(x: Int): Future[Int] = async { Thread.sleep(1000) x } def addAsync(x: Future[Int], y: Future[Int]): Future[Int] = async { await(x) + await(y) } println( Await.result(add(getInt(1), getInt(2)), 10.seconds) )
いかがでしょうか。await
関数を利用することで、Future
の値をあたかも同期処理のように記述することができ、見やすいコードになったと思います。
await
はただの関数ですので、C#同様に式が書けるところにはどこでも書くことができます。(C#のawait
はasync
をつけたメソッド内にしか書けないことと同様、Scalaのawait
もasync
関数の中にしか書けません。)
このため、多くのFuture
を扱う場面においても、見やすいコードが書けるのではないかと思っています。早く標準ライブラリに入って欲しい・・・!
使い方
依存ライブラリに追加して、関数をimportするだけで利用できます。
(sbt)
libraryDependencies += "org.scala-lang.modules" %% "scala-async" % "0.9.2"
(import)
import scala.async.Async.{async, await}
なお、実装はマクロとコンパイラプラグインで頑張っている模様です。
(追記)実装はマクロのみでした。失礼しました。コメントくださったxuwei_kさん、ありがとうございます!
制限
関数の引数が名前渡しの場合、awaitの戻り値をそのまま渡せないようです。
def increment(x: => Int): Int = x + 1 async { increment( await(getIntAsync(0)) ) }
この場合は、await
の結果を一旦変数に入れてから利用します。
async { val x = await(getIntAsync(0)) increment(x) }
その他の制限については、GitHubのプロジェクトやIssuesを確認してみてください。
まとめ
C#のasync
、await
をScalaで利用できるライブラリを紹介しました。SIP 22で提案はされていますがPendingとなっており、SIPを詳しく追いかけているわけではないため、現在どのようなステータスとなっているかまでは分かりません。
仕事ではScalaよりもC#を使っているため、多少のバイアスはかかっていますが、今回の記事で紹介したとおり、コードが見やすくシンプルになると思います。ぜひ標準ライブラリ入りして欲しいですね!
気になる方はぜひ使ってみてください。