2013年9月30日月曜日

akkaのActorを抽象化する方法を試行錯誤する

akkaを使ってActorをゴリゴリ作ってると、当然Actorを抽象化したいという要求が出てくるよね。

例えばこんな方針で抽象化を試みる。

  • Actorを継承したtraitを作って、共通処理を抽象化する。
  • 作ったtraitは型パラメータを持たせて、具象クラス化するときに例えば受け取るメッセージの型を指定する。

import akka.actor._
trait AbstractActor[MesType] extends Actor {
override def receive = ...
}
case class MyMessage(mes: String)
class MyActor extends AbstractActor[MyMessage]
view raw 01.scala hosted with ❤ by GitHub


なんでわざわざ型パラメータなんて持たせてるかというと、これをやりたい。

import akka.actor._
import scalaz._, Scalaz._
trait AbstractActor[MesType] extends Actor {
implicit val MesShow: Show[MesType]
override def receive = {
case m: MesType => m.println // コンパイルエラーだけどこういうことがやりたい
}
}
case class MyMessage(mes: String)
class MyActor extends AbstractActor[MyMessage] {
override val MesShow = Show.showA[MyMessage]
}
view raw 02.scala hosted with ❤ by GitHub


要するに、送受信するメッセージは処理を内包しないただの型(case class MyMessage)にしといて、その型の挙動は型クラスのインスタンス(MesShow)で制御したいというわけ。

ところが、この実装には問題がある(実際に上記コードはコンパイルが通らない)。

  • akkaのActorにおいてメッセージはAny型で渡されるのでRuntimeにしか型判定できない
  • traitの型パラメータはコンパイル時に型消去されるのでRuntimeでの判定には使えない

=> つまり、上記コードの「case m: MesType」という判定はできない、となる。

これを解決するためには、

  • Anyで渡ってくるメッセージはobj.getClassを経由してRuntimeにクラス情報を取得する
  • 型パラメータの方は、TypeTagを渡すことでRuntimeまでクラス情報を引き継ぐ

とやるしかない、と思う。たぶん。

で、書いてみたのが以下のコード。

import akka.actor._
import scalaz._, Scalaz._
import scala.reflect.runtime.universe._
trait AbstractActor[MesType] extends Actor {
implicit val TTag: TypeTag[MesType]
implicit val MesShow: Show[MesType]
override def receive = {
case m if getType(m) <:< typeOf[MesType] => {
val mes = m.asInstanceOf[MesType]
mes.println
}
}
def getType(obj: Any): Type = {
val clazz = obj.getClass
val mirror = runtimeMirror(clazz.getClassLoader)
mirror.classSymbol(clazz).toType
}
}
case class MyMessage(mes: String)
class MyActor extends AbstractActor[MyMessage] {
override val TTag = typeTag[MyMessage]
override val MesShow = Show.showA[MyMessage]
}
view raw 03.scala hosted with ❤ by GitHub


TypeTagをわざわざ指定してやらないといけないところとか、asInstanceOfでダウンキャストしてるところとか、カッコ悪い。

というわけで、「こうやるともっと綺麗に書けるよ」とか、「そもそも方針としてこうやるべき」とかあったら教えてください!

2013年9月16日月曜日

台風で身動きできなくなったからTDD勉強会開いた

昨日(9/15)から大学時代の友人である@tau06のウチに@minismahnと泊まりで遊びに行ったら、予想通り今日(9/16)の午前中は見事に台風直撃で電車動いてなくて身動き取れなくなったので、TDD勉強会などを開いてみた。

7月のTDDBC東京のときのお題、飲み物自動販売機 Ver 2.0をお借りして、実践しつつTDDってこうやってやるんだよー的なことを説明してから、2人でコード書いてもらった。
2人とも、別に俺が隅から隅まで教えなくても勝手にどうやったらいいか考えてやってくれるってのはわかってた。で、予想通り後半暇になってきたので、俺ももう1回改めて同じ課題をやってみた。

やっぱりStateモナドが正しかった


7月のとき、最初にお題見た時から「これStateじゃね?」とは思っていたんだけど、僕は初対面の人とペアプロやっていきなり「Stateモナドで行きましょう」とか言い出すこわい人じゃないので、さすがにやらなかったんですね(@razonさんにそれを言っても問題なかったかもしれないけど)。

というわけで、今日書いたコード全貌はこちら。
https://gist.github.com/nisshiee/6578338

ある瞬間のスナップショットとしてのVendingMachine型を用意するところまでは7月と一緒。で、前回はここから「VendingMachineを引数に受け取って、次の状態を表すVendingMachineを返す関数」を実装していったわけだが、今回は「State[VendingMachine, _]」を実装していく。あ、言い忘れたけどStateはScalaz使う。

Stateを作るのは簡単で、Stateオブジェクトのapplyメソッドを使えば良い。
引数fには、「処理前の状態Sを受け取り、処理後の状態S'と処理に伴って出力される値Aのタプルを返す」関数を渡してやれば良い。

def apply[S, A](f: (S) ⇒ (S, A)): State[S, A]
view raw 01.scala hosted with ❤ by GitHub


例えば、現在の投入金額合計を取得する処理は、状態を表すVendingMachineには変化を与えず、金額だけをIntとして取得するStateとなるので、以下の様になる。

val total = State[VendingMachine, Int] { vm => (vm, vm.total) }
view raw 02.scala hosted with ❤ by GitHub


コインを投入する処理の場合は、VendingMachineの投入金額合計を加算して、処理に伴う出力は特に無いStateとなる。ただし、投入するコインの種類によって加算する額を変えたいので、一段高階にする。つまりStateそのものをvalで定義するのではなく、「Money => State」を定義する。

val insert: UsableMoney => State[VendingMachine, Unit] = m => State { vm =>
(vm.copy(total = vm.total + m.value), ())
}
view raw 03.scala hosted with ❤ by GitHub


で、用意したStateを実行(?)するときは、まずいつもどおりモナドをfor式なりmapなりflatMapなりで結合して、最後にapply(処理後のSとAを両方取得)なり、eval(処理後のAを取得)なり、exec(処理後のSを取得)なりのメソッドに"初期状態"を渡してやれば良い。
「100円入れて、10円入れて、そのあとの投入金額合計を取得」はこうなる。

(insert(Money100) >> insert(Money10) >> total) eval VendingMachine.init // = 110
// もしくはfor式で書くならこう
(for {
_ <- insert(Money100)
_ <- insert(Money10)
t <- total
} yield t) eval VendingMachine.init // = 110
view raw 04.scala hosted with ❤ by GitHub


さいごに


この自動販売機のお題、オブジェクト指向で順番にやっていくと、内部状態が関わる関係でいろいろややこしい状況に陥る面白いお題になっている(やってみるとわかる)。
でもStateモナドを使ったらどうなるだろうか。僕が書いたテストコード最初から最後まで一貫してStateモナドの枠の中で収まっている(実装しておいたStateを、1そのまま、2演算子で結合、3for式で結合のどれかして、evalかけているだけ。全テストケースとも。)

モナド、面白いね!

2013年9月3日火曜日

Dispatch(reboot)で文字化けが起きた時の対応

今日これで30分ぐらい無駄にしたので忘れる前にメモ。

概要


Dispatchを使ってHTTPクライアントを作ると、稀に文字化けすることがあるので、その原因と対策をまとめる。
ちなみにDispatchはAsyncHttpClientを内部で使っていて、今回の話題はAsyncHttpClientの領域にも少し踏み込む。

原因と解決方針


文字化けの原因はHTTPクライアントにはよくある話で、HTTPレスポンスヘッダのContent-Typeにcharsetがセットされていない(または間違っている)のが原因。
Bodyバイト列をStringに変換するときにContent-Typeのcharsetを用いるのだが、デフォルトではISO-8859-1が使われるため、charset未設定のヘッダが来ると、bodyの文字コードが例えUTF8であっても化けてしまう。

で、これの解決方法として、ResponseFilterを使う。
ResponseFilterはAsyncHttpClient側の機能で、bodyの解析前に処理を挟むことができる。これを用いて、ヘッダのContent-Typeにcharsetを書き込んでしまえば良い。

DispatchからResponseFilterを挟む


あとは実際にコードを書くだけだが、今回はDispatchからの利用なのでDispatchのAPIからResponseFilterを挟む必要がある。実際にコードを見たほうが早いだろう。

import dispatch._
import com.ning.http.client.filter.{ ResponseFilter, FilterContext }
import scala.collection.JavaConverters._
val MyHttp = Http.configure { builder =>
builder.addResponseFilter(new ResponseFilter {
override def filter(ctx: FilterContext[_]) = {
ctx.getResponseHeaders.getHeaders.get("Content-Type").asScala.toList match {
case "text/html" :: Nil =>
ctx.getResponseHeaders.getHeaders.put("Content-Type", List("text/html; charset=utf-8").asJava)
case _ => ()
}
ctx
}
})
}
MyHttp(url("http://example.com") OK as.String)
view raw myhttp.scala hosted with ❤ by GitHub


Dispatchで提供されるデフォルトのHttpはconfigureメソッドを持っており、これにAsyncHttpClientConfig.Builderをいじる関数を渡してやることで、HTTPクライアントの挙動を制御することができる。今回はaddResponseFilterを使ってResponseFilterを挟んでやろうと言うわけだ。ResponseFilterの実装はcharset無しのContent-Typeが渡ってきたらcharset付きで上書きしてやろうというもの。Scalaの感覚で「Filter」というとFilterContextを受け取って新しいFilterContextを生成する実装をしたくなるが、ここでは単純にmutableなオブジェクトの操作でOK。

この例では"text/html"の時だけUTF-8を指定しているだけだが、必要に応じて実装を書き換えてやれば良い。

参考


Http.configureについてはこちら、AsyncHttpClientConfig.Builderについてはこちら、ResponseFilterについてはこちらを参照。