Scala の Future と Promise で Android の Callback 地獄を緩和した話

Android というより Java のライブラリで頻出する Callback パターン呼び出し連鎖

def setImage(id: Stiring) = {
  User.load(id, new OnLoadCallback {
    override def onSuccess(userJson: String) {
      Json.parse[User](userJson, new JsonParseCallback {
        override def onSuccess(user: User) {
          Image.load(user.imageUri, new OnLoadCallback {
            override def onSuccess(image: Image) {
              view.setImage(image)
            }
          }
        }
      }
    }
  }
} // >つらい<

みたいのを Scala 標準のライブラリに含まれる Future と Promise で殺す話。
いわゆる Listener についてはこの方法だと解決しないので、このへんを参照してほしい。

とりあえず結論として殺した例

def userLoadFuture(id: String): Future[String] = {
  p = Promise[String]()
  f = p.future
  User.load(id, new OnLoadCallback {
    override def onSuccess(userJson: String) {
      p.success(userJson)
    }
  }
  f
}

def jsonParseFuture(userJson: String): Future[User] = ??? // 上と同じなので省略
def imageLoadFuture(user: User): Future[Image] = ???

val imageFuture = for { // Callback ネストを for式で置き換える
  userJson <- userLoadFuture(id)
  user <- jsonParseFuture(userJson)
  image <- imageLoadFuture(user)
} yield image

imageFuture.foreach(view.setImage(image))

なにが起こっているのか

そもそも Scala の Future については非同期便利ライブラリとして比較的有名かと思う。Future でつつむとその部分を非同期でよしなに処理してくれるんだけど、要は Callback パターンを隠蔽している感じ。

jikanKakaruFuture: Future[String] = Future(jikanKakaruFunc(id))

jikanKakaruFuture.foreach(println) // Callback関数を渡しているイメージ

なので、Scala のライブラリとかで時間のかかる処理はたいてい戻り値が Future になっていたりする。ただ、Java のライブラリとかを使ってるとそうもいかなくて、どうしても Callback パターンに遭遇する。Callback 自体は Non-brocking で非同期なのでいいんだけど、前述のとおりネストしてくるとかなりつらい(実際にはエラー処理もはいるのでもっとつらい)。

そこで Java の Callback を Future でなんとかできないか?!??!!!!??!??!!?!と当然思うわけだけど、Future だけではうまくいかないことにすぐに気づく。そもそも Callback だと返り値もへったくれもないですよね…

そこで、Promise が登場する。Future はいろんなところで出てくるけど Promise は OE さんの記事 を見るまで存在すら知らなかった。この記事も2015 Dec. 15th 公開だし知らなくても仕方ないね!!!

OE さんの記事とか Future と Promise 関連で調べてわかったのは、Promise は値未定の Future を返すことができるということと、自分が返した Future に対して値を書き込めるということ。Future が値未定なのは当たり前なんだけど、Future は普通にファクトリメソッドで作るとそこで処理を決定しなきゃいけないけれど、Promise を使うと別のタイミングで Future に値を書き込むことができる。同じことを2回書いている気がする。

val normalFuture: Future[String] = Future("ここで返す値を決める")
val p = Promise[String]() // Promise 自体は値を要求しない
val promiseFuture = p.future // 値の返し方を決定していない Future を返す

p.success("ここで返す値を決める。Future の生成タイミングではない")

normalFuture.foreach(println)
promiseFuture.foreach(println)

というわけで、Promise のこの特性(あと Future が flatMap でつなげること)を利用して、Callback のネストを for式に置き換えることができるということでした。ただし、Future は imuutable な設計なので、一度値を決定すると破壊的再代入はできないから、今回みたいな Callback ネストには良いけど、Listener みたいな何度も呼ばれるやつには使えないのでした。

コード再掲。

def userLoadFuture(id: String): Future[String] = {
  p = Promise[String]()
  f = p.future
  User.load(id, new OnLoadCallback {
    override def onSuccess(userJson: String) {
      p.success(userJson)
    }
  }
  f
}

def jsonParseFuture(userJson: String): Future[User] = ??? // 上と同じなので省略
def imageLoadFuture(user: User): Future[Image] = ???

val imageFuture = for { // Callback ネストを for式で置き換える
  userJson <- userLoadFuture(id)
  user <- jsonParseFuture(userJson)
  image <- imageLoadFuture(user)
} yield image

imageFuture.foreach(view.setImage(image))

よかったね。

参考:
Composable Callbacks & Listeners
Scala標準のPromiseがAndroidで便利だという話
Promise – scaladoc

広告

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中