2012年11月15日木曜日

ScalaTips / Map, Function1, PartialFunction, Option

クライアントから受け取ったリクエストや定義ファイルの情報などをMapに格納して、プログラム内で持ちまわることはよくあります。

このような場合、以下のように関数の引数にMapを渡します。

def f(config: Map[String, String]) {
  println(config("name"))
  println(config("url"))
}

以下のようなMapがある場合:

scala> val map = Map("name" -> "Taro", "url" -> "http://www.example.com")
map: scala.collection.immutable.Map[java.lang.String,java.lang.String] = Map(name -> Taro, url -> http://www.example.com)

関数fの動作は以下のようになります。

scala> f(map)
Taro
http://www.example.com

MapはFunction1

JavaだとMapを受け取るメソッドを書いたところで終了ですが、Scalaの場合MapはFunction1という特徴があるので、もう少し応用範囲が広がります。

以下の関数fでは引数がMapではなくFunction1になっています。

def f(config: String => String) {
  println(config("name"))
  println(config("url"))
}

ScalaのMapはFunction1でもあるので、以下のようにこの関数にMapを指定することができます。

scala> f(map)
Taro
http://www.example.com

関数fの設計を考える場合、Function1がMapを包含しているわけですから、Function1を引数に取ったほうが応用範囲が広がり、より望ましい選択と言えます。

値がない場合を考慮

関数の引数にMapを取る場合、Mapの要素として指定した値がないことを考慮したい場合があります。この時はOptionを返すgetメソッドを用いて処理を切り分けます。

def f(config: Map[String, String]) {
  config.get("name").foreach(println)
  config.get("url").foreach(println)
}

値がある場合は以下のようになります。

scala> f(map)
Taro
http://www.example.com

値がない場合もきちんと動作します。

scala> f(Map.empty)

PartialFunction

値がない場合を扱うことができる関数としてScalaではPartialFunctionを提供しています。PartialFunctionのisDefineAtメソッドとapplyメソッドを駆使すると以下のように値がない場合の切り分け処理を記述することができます。

def f(config: PartialFunction[String, String]) {
  if (config.isDefinedAt("name")) {
    println(config("name"))
  }
  if (config.isDefinedAt("url")) {
    println(config("url"))
  }
}

ScalaのMapはFunction1であると同時にPartialFunctionでもあります。このため、この関数fにもそのまま指定することができます。

scala> f(map)
Taro
http://www.example.com

値がないときも無事動作しました。

scala> f(Map.empty)

PartialFunctionをFunction+Optionにlift

先ほどの関数fは、PartialFunctionの扱いが手続き型チックだったので、もう少し関数型っぽくしてみます。

具体的にはliftメソッドを用いて、PartialFunction[String, String]をFunction1[String, Option[String]]に持ち上げます。

def f(config: PartialFunction[String, String]) {
  val c: String => Option[String] = config.lift
  c("name").foreach(println)
  c("url").foreach(println)
}

実装は変わりましたが、インタフェースは変わらないのでMapを指定しても同じように動作します。

scala> f(map)
Taro
http://www.example.com

scala> f(Map.empty)

Option

今度は逆に、関数fの引数がFunction1[String, Option[String]]だった場合です。

def f(config: String => Option[String]) {
  config("name").foreach(println)
  config("url").foreach(println)
}

この場合は、Map[String, String]を指定するとエラーになってしまいます。

scala> f(map)
<console>:10: error: type mismatch;
 found   : scala.collection.immutable.Map[java.lang.String,java.lang.String]
 required: String => Option[String]
              f(map)
                ^

ここで登場するのがPartialFunctionのliftメソッドです。MapもPartialFunctionなので、このliftメソッドを使ってFunction1[String, Option[String]]に持ち上げることができます。

以下のように無事関数fに適用できました。

scala> f(map.lift)
Taro
http://www.example.com

ノート

日々のScalaプログラミングでよく出てくるMap, Function1, PartialFunction, Optionの連携を簡単にまとめてみました。

MapがFunction1かつPartialFunctionであるということは案外盲点で、このことを知っておけば関数のシグネチャでMapより応用範囲の広いFunction1やPartialFunctionを選択できるようになります。

また、Mapが出てくるような局面では値がない場合があることが普通なので、Function1よりもPartialFunctionがより望ましい選択になります。PartialFunctionが出てくるとOptionの活用も視野に入ってきます。

関数の引数にPartialFunctionを使うのか、Function1+Optionを使うのかはケースバイケースですが、Mapのliftメソッドの存在を知っておけば、どちらがきた場合でも対処できます。

諸元

  • Scala 2.9.2

0 件のコメント:

コメントを投稿