ダウンキャスト

Scala でプログラミングしていると型パラメータやパターンマッチがあるからか、ダウンキャストが必要になることがあまり無いです。

なのですが、パターンマッチの多用によりインデントが深くなってしまうのが悩ましかったりします。
例えば dispatch.json (sjson で利用している JSON のパーサ等) を利用して、すでに型がわかっているデータ構造をたどっている場合などです。 *1

Js(response) match {
  case JsObject(x) => x.get(JsString("foo")) match {
    case Some(JsString(foo)) => x.get(JsString("bar")) match {
      case Some(JsString(bar)) => Some((foo, bar))
      case _ => None
    }
    case _ => None
  }
  case _ => None
}

読みづらすぎる*2・・・ case _ => None の出現回数が多いのとインデントが深いのとで、なんだかいまいちです。

Scala には for 内包表記という便利なものがあるので、それを使用してなんとかインデントを浅くできないものかと思い、以下のように書いてみました。

for {
  JsObject(x)   <- Some(Js(response))
  JsString(foo) <- x.get(JsString("foo"))
  JsString(bar) <- x.get(JsString("bar"))
} yield (foo, bar)

あぁ、すっきり。
なのですが、JsObject(x) <- ... や JsString(foo) <- ... のようにして受けているため、もし型が違っていた場合は例外がでてしまいます。

なんとかして想定と違う型の場合はそのデータは無視して処理を続行したいです。
というわけで、以下のような型検査&変換をするメソッドを作ってみました。

implicit def any2Caster[T](value: T): Caster[T] = new Caster[T](value)
class Caster[T](value: T) {
  def as[U<:T:Manifest]: Option[U] =
    if (manifest[U].erasure.isInstance(value)) Some[U](value.asInstanceOf[U])
    else None
}

使用例は以下のような感じになります。

val value: Any = "Hello World" // value は Any 型
val text = value.as[String]    // text は Option[String] 型、値は Some("Hello World")
val integer = value.as[Int]    // integer は Option[Int] 型、値は None

この as[T] と JsObject -> Map、String -> JsString の暗黙の型変換を定義しておくことで、以下のような記述ができます。

for {
  x   <- Js(response).as[JsObject]
  foo <- x.get("foo").flatMap(_.as[JsString])
  bar <- x.get("bar").flatMap(_.as[JsString])
} yield (foo, bar)

あるいは flatMap の記述も排除するならば以下のようにします。。

for {
  x      <- Js(response).as[JsObject]
  foo    <- x.get("foo")
  bar    <- x.get("bar")
  fooStr <- foo.as[JsString]
  barStr <- bar.as[JsString]
} yield (fooStr, barStr)

ちなみに Js* との型変換は以下のような感じで定義しています。

implicit def jsObject2Map(js: JsObject): Map[JsString, JsValue] = js.self
implicit def jsString2String(js: JsString): String = js.self
implicit def string2JsString(value: String): JsString = JsString(value)

def as[T]:Option[T] は Scala に標準で用意されていても良さそうな気もするのですが、私が知らないだけなのかも・・・?

*1:本来であれば sjson で Protocol 書いて、対応する Scala の型にマップしてしまえば良いです

*2:わざとパターンマッチだけで記述しています