Scala.jsのFutureとPromiseを使ってみる 2014年03月07日

Scala.jsFiddle というScala.jsをWebで編集して実行できるという面白いサイト最近できたのですが、 そのソースを見ていたらScala.js 0.3から入ったFutureとPromiseがわかりやすい形で使われていたので、その部分を取り出してちょっと使ってみることにしました。

今回作ったサンプルコードは以下にあります。

hexx/scala-js-future-example

XMLHttpRequestをFutureでラップするというコードです。

サーバ

サーバ側から見ますと、

// server/src/main/scala/Server.scala
import unfiltered.request._
import unfiltered.response._
import unfiltered.jetty._
import unfiltered.filter._

object ACAO extends HeaderName("Access-Control-Allow-Origin")

object Json {
  def apply(json: String) = new ComposeResponse(JsonContent ~> ResponseString(json))
}

object Server extends App {
  Http(8080).filter(Planify {
    case GET(Path("/")) =>
      Ok ~> ACAO("*") ~> Json("""{"name":"Martin"}""")
    case req @ POST(Path("/")) =>
      Ok ~> ACAO("*") ~> Json(s"""{"posted":"${Body.string(req)}"}""")
  }).run()
}

unfilteredを使ったコードで、 GET / で {"name":"Martin"} というJSONを、 POST / で {"posted":"ポストされたデータ"} というJSONを返します。

クライアント

クライアント側のXMLHttpRequestをラップするコードは以下のような感じです。Scala.jsのコードなのでScalaで書かれていますが、JavaScriptに変換されます。

// client/src/main/scala/Ajax.scala
package com.github.hexx

import scala.scalajs.js
import scala.concurrent.{Promise, Future}
import org.scalajs.dom.{XMLHttpRequest, Event}

case class AjaxException(xhr: XMLHttpRequest) extends Exception

object Ajax {
  def get(url: String): Future[XMLHttpRequest] = apply("GET", url)

  def post(url: String, data: String): Future[XMLHttpRequest] = apply("POST", url, Option(data))

  def apply(method: String, url: String, data: Option[String] = None): Future[XMLHttpRequest] = {
    val req = new XMLHttpRequest()
    val promise = Promise[XMLHttpRequest]

    req.withCredentials = true

    req.onreadystatechange = (e: Event) => {
      if (req.readyState.toInt == 4) {
        if (200 <= req.status && req.status < 300) {
          promise.success(req)
        } else {
          promise.failure(AjaxException(req))
        }
      }
    }

    req.open(method, url, true)

    data match {
      case Some(d) => req.send(d)
      case None => req.send()
    }

    promise.future
  }
}

XMLHttpRequestとPromiseの両方を知ってる人にとってはわかりやすいコードだと思います。 Promiseはsuccessメソッドか、failureメソッドを使ってFutureの値を確定することができます。 その処理をXMLHttpRequestのonreadystatechangにコールバックで登録しています。

使い方

使い方は以下のような感じです。

// client/src/test/scala/AjaxTest.scala
import scala.scalajs.js
import scala.scalajs.test.JasmineTest

import scala.concurrent.Await
import scala.concurrent.duration.Duration

import com.github.hexx.Ajax

import scala.scalajs.concurrent.JSExecutionContext.Implicits.runNow

object HtmldaJsJasmineTest extends JasmineTest {
  describe("Futures and Promises") {
    it("should work on Scala.js.") {
      val json = for {
        res1 <- Ajax.get("http://localhost:8080/")
        json = js.JSON.parse(res1.responseText)
        res2 <- Ajax.post("http://localhost:8080/", s"${json.name}-san")
      } yield js.JSON.parse(res2.responseText)

      val posted = Await.result(json, Duration.Zero).posted

      expect(posted).toEqual("Martin-san")
    }
  }
}

ScalaのFutureはfor構文を使うことができます。まず、 GET / をしてJSONを受け取り、その結果を POST / に投げています。その一連の処理がfor構文に書かれています。

JavaScriptでのFutureの実行は二種類あります。JavaScriptはマルチスレッドではないので、 import scala.scalajs.concurrent.JSExecutionContext.Implicits.runNow の場合はその場で実行されて、 mport scala.scalajs.concurrent.JSExecutionContext.Implicits.queue の場合は setTimeout 0 で実行されるということになります。

結構ScalaのFutureがそのまま使える感じで、いいのではないかと思います。 Scala.jsFiddleのコードでは、FutureのApplicative構文のようなものが使えるScala Asyncも使っていました。

scala/async

ただ、テスト環境ではEnvJSを使っているんですが、異常系が動かないことが多くて困りました。実際のブラウザ環境ならもっと動くと思うんですが、まだ調査が足りてない感じです。