Scala が解決できる Android アプリ開発の問題領域について - 非同期処理編

More than 1 year has passed since last update.

副題 : Scala の for-yield の強力さについて

おさらい 1 : for-yield ってどんなやつ

// Scala に慣れてる人は次の節まで読み飛ばしてください :)

describe("例1: Option の for-yield") {
  it("1-1: 中身の Int を足し合わせる") {
    def sum(valueA: Option[Int], valueB: Option[Int], valueC: Option[Int]): Option[Int] =
      for {
        a <- valueA
        b <- valueB
        c <- valueC
      } yield { a + b + c }

    sum(Some(1), Some(10), Some(100)) shouldBe Some(111)
  }
}
describe("例2: List の for-yield") {
  it("2-1: 中身の Int を足し合わせる") {
    def sum(xs: List[Int], ys: List[Int]) =
      for {
        x <- xs
        y <- ys
      } yield { x + y }

    sum(List(1,2), List(10,20)) shouldBe List(11,21,12,22)
  }
}

for-yield を使って Option や List から中の値 ( a,b,c と x,y ) を取り出して操作する例を挙げてみました。これは、コンパイラによって for-yield が下記のようなメソッド呼び出しに置き換えられることで実現されています。

describe("例3: flatMap & map") {
  it("3-1: Option[Int] を足し合わせる") {
    def sum(valueA: Option[Int], valueB: Option[Int], valueC: Option[Int]): Option[Int] =
      valueA flatMap { a =>
        valueB flatMap { b =>
          valueC map { c =>
            a + b + c
          }
        }
      }

    sum(Some(1), Some(10), Some(100)) shouldBe Some(111)
  }
  it("3-2: List[Int] を足し合わせる") {
    def sum(xs: List[Int], ys: List[Int]) =
      xs flatMap { x =>
        ys map { y =>
          x + y
        }
      }

    sum(List(1,2), List(10,20)) shouldBe List(11,21,12,22)
  }
}

for-yield が flatMap と map のネストで表現されていることに注目してください。これは地味にとても重要なポイントで、逆に言うなら「flatMap と map さえ適切に用意できるのならネストされた構造は for-yield に変換できる」ということです。

おさらい 2 : Android の非同期処理ってどんなやつ

// Android 開発に慣れてる人は次の節まで読み飛ばしてください :)

公式にちょうどいい ドキュメント が用意されていたので抜粋します。

public void onClick(View v) {
    new Thread(new Runnable() {
        public void run() {
            final Bitmap bitmap = loadImageFromNetwork("http://example.com/image.png");
            mImageView.post(new Runnable() {
                public void run() {
                    mImageView.setImageBitmap(bitmap);
                }
            });
        }
    }).start();
}

別スレッドで画像をダウンロードしてそれを画面内に表示するというありふれた例ですが

  • ユーザの操作をブロックしないようにダウンロードを待つためのスレッドと
  • 画像を画面内に表示するためのスレッド

という操作を分けるために複数個の new Runnable ... が必要になっています。さすがにこの悲しみ漂うコードを書くのはしんどいということで、対処策として SDK に用意されているのが AsyncTask というクラスです。

public void onClick(View v) {
    new DownloadImageTask().execute("http://example.com/image.png");
}
private class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {
    protected Bitmap doInBackground(String... urls) {
        return loadImageFromNetwork(urls[0]);
    }
    protected void onPostExecute(Bitmap result) {
        mImageView.setImageBitmap(result);
    }
}

これによって「UI は安全に、コードはシンプルになりました」とのことですが、正気か。
このコードはまだ多くの問題を抱えています。

  • 非同期で実行したい処理と、その結果を必要とするコードが密結合になる
    • 今回の例では、画像のダウンロード処理を :
      • 他の場所で再利用できない
      • 単体テストすることができない
      • 画像表示のための (デバッグやテストのために) ダミーに差し替えることができない
  • 非同期の処理が終わったタイミングについて呼び出し側では分からない
    • 今回の例では、画像のダウンロード後に :
      • プログレスバーを非表示にするといった処理を差し込むことが不可能
      • さらに別な画像のダウンロードを開始するといった処理を足すことも不可能

おさらい 3 : Observer パターンについて

前項のような問題点を解決するための方法の一つが Observer パターンと呼ばれる設計で、これは平たく言えば「何かが終わったタイミングでイベントを飛ばすからそれを使ってよしなにやってくれや」というものです。結果を受け取って次の処理を開始するのは、イベントリスナやイベントハンドラと呼ばれる observer にあたるもので、イベントを飛ばす本体はその処理については何も関知しません。

これを踏まえて先ほどのコードを Scala で書き換えてみましょう。イベントリスナはコールバック関数として表現すれば話は済みそうです。

def onClick(v: View): Unit = {
  downloadImage("http://example.com/image.png")(showImage)
}
def showImage(bitmap: Bitmap): Unit = {
  mImageView.post(new Runnable {
    override def run(): Unit = mImageView.setImageBitmap(bitmap)
  })
}
def downloadImage(url: String)(callback: Bitmap => Unit): Unit =
  new Thread(new Runnable {
    override def run(): Unit = {
      val bitmap = loadImageFromNetwork(url)
      callback(bitmap)
    }
  }).start()

画像をダウンロードするための関数には、画像を表示する処理については何も書かれていません。呼び出し側がコールバック関数を自由に渡せるようになったため、先ほど挙げた問題点はいずれも解決できていることが分かります。

ちょっと本題から逸れますが、new Runnable や new Thread などの煩雑な記述を避けるために、Scala ではもう少しシンプルに書き直せます。

def onClick(v: View) = {
  downloadImage("http://example.com/image.png")(showImage)
}
def showImage(bitmap: Bitmap): Unit = {
  mImageView post runnable { mImageView.setImageBitmap(bitmap) }
}
def downloadImage(url: String)(callback: Bitmap => Unit): Unit = async {
  val bitmap = loadImageFromNetwork(url)
  callback(bitmap)
}
def runnable[A](f: => A) = new Runnable {
  override def run(): Unit = f
}
def async[A](f: => A) = new Thread(runnable(f)).start()

これにて一件落着。
めでたしめでたし…と言いたいところですが、まだです。ここまではおさらいです。

本題 : 非同期の処理が終わったら別な非同期の処理を開始したい

今回解決したかった問題領域の話に入ります。ありがちなパターンとはいえなかなか厄介です。
前節の例を利用して、複数個の画像を順番にロードする場合について考えてみましょう。

def onClick(v: View): Unit = {
  downloadImage("http://example.com/image1.png")(showImageTo(mImageView1))
  downloadImage("http://example.com/image2.png")(showImageTo(mImageView2))
  downloadImage("http://example.com/image3.png")(showImageTo(mImageView3))
}
def showImageTo(imageView: ImageView)(bitmap: Bitmap): Unit = {
  imageView post runnable { imageView.setImageBitmap(bitmap) }
}

これでは一度に全てのダウンロードが開始されてしまうのでもちろんダメです。
正しくはこうなります。

def onClick(v: View): Unit =
  downloadImage("http://example.com/image1.png"){ x =>
    showImageTo(mImageView1)(x)

    downloadImage("http://example.com/image2.png"){ y =>
      showImageTo(mImageView2)(y)

      downloadImage("http://example.com/image3.png"){ z =>
        showImageTo(mImageView3)(z)
      }
    }
  }

Scala だからこの量で済んだと好意的な言い方もできますが、まだ嫌な感じがします。
何かが足りていません。そうです。この形はつい先ほど目にしたアレです。

flatMap と map さえ適切に用意できるのならネストされた構造は for-yield に変換できる

結論 : コールバック関数による非同期処理は for-yield でつなげることができる

長い前振りでしたが、下記のようなクラスを用意すればこのコールバック関数ネスト問題については解決です。

class CallbackTask[EVENT](callback: (EVENT => Unit) => Unit){
  def map[A](f: EVENT => A): CallbackTask[A] =
    new CallbackTask[A](g => execute(f andThen g))

  def flatMap[A](f: EVENT => CallbackTask[A]): CallbackTask[A] =
    new CallbackTask[A](g => execute(e => f(e) execute g))

  def execute(onFinish: EVENT => Unit): Unit = callback(onFinish)
  def execute(): Unit = execute(_ => ())
}
object task {
  def by[A](f: => A) = new CallbackTask[A](_(f))
  def to[A](f: (A => Unit) => Unit) = new CallbackTask[A](f)
}

非常にシンプルなコードですがとても強力です。
これを使うとさっきの微妙なコードは下記のように変わります。

def onClick(v: View): Unit = {
  val callback = for {
    x <- task to downloadImage("http://example.com/image1.png")
    _ <- task by showImageTo(mImageView1)(x)
    y <- task to downloadImage("http://example.com/image2.png")
    _ <- task by showImageTo(mImageView2)(y)
    z <- task to downloadImage("http://example.com/image3.png")
    _ <- task by showImageTo(mImageView3)(z)
  } yield ()
  callback.execute()
}

もうネストの苦しみに耐える必要はありません。
処理を継ぎ足すことも順序を入れ替えることも自由自在になりました。

補足.1

IntelliJ には for-yield を展開する機能があるので、
( opt+enter > Convert for comprehension to desugared expression )
どんなコードが生まれているのかお手軽に確認できたりします。

def onClick(v: View): Unit = {
  val callback = task to downloadImage("http://example.com/image1.png") flatMap {
    case x =>
      task by showImageTo(mImageView1)(x) flatMap {
        case _ =>
          task to downloadImage("http://example.com/image2.png") flatMap {
            case y =>
              task by showImageTo(mImageView2)(y) flatMap {
                case _ =>
                  task to downloadImage("http://example.com/image3.png") flatMap {
                    case z =>
                      task by showImageTo(mImageView3)(z) map {
                        case _ => ()
                      }
                  }
              }
          }
      }
  }
  callback.execute()
}

わーお
(スレッドを生成しないような処理では乱用注意ですね)

補足.2

Scalaz には Future.async や Task.async というものがあって、i/f を見る限りではこれとほとんど同じかそれ以上の機能を有しているようです。必要がなかったので今回は追跡していません…(誰かおねがいします)

おわり

Android アプリも Scala で開発すれば楽しいですよ!