2012年11月19日月曜日

Scala Tips / Seq, Function1, PartialFunction, Option

MapがFunction1かつPartialFunctionというのが案外盲点になりますが、SeqすなわちList, Stream, VectorもFunction1かつPartialFunctionというのも見過ごしがちです。

以下の関数fを考えます。「Intを引数に取りStringを返す関数」を第一引数に、Int値を第二引数に取り、Int値に対応するStringを返します。

def f(a: Int => String)(b: Int): String = {
  a(b)
}

準備

Seqの例としてListを定義します。

scala> val l = List("zero", "one", "two", "three")
l: List[java.lang.String] = List(zero, one, two, three)

ついでにMapも定義しておきます。

cala> val m = Map(4 -> "four", 5 -> "five")
m: scala.collection.immutable.Map[Int,java.lang.String] = Map(4 -> four, 5 -> five)

関数fの第一引数にListを指定するとList(Seq)はFunction1のためそのまま普通に動作します。

ListのインデックスがFunction1の引数に対応しており、引数に指定された数値に対応するListの内容を返します。

scala> f(l)(3)
res6: String = three

Map, Function1, PartialFunction, Option」で説明したようにMapもFunction1なので普通に動作します。

scala> f(m)(5)
res7: String = five

Listの範囲外の数値を指定すると例外が発生します。

scala> f(l)(10)
java.lang.IndexOutOfBoundsException: 10
 at scala.collection.LinearSeqOptimized$class.apply(LinearSeqOptimized.scala:51)
 at scala.collection.immutable.List.apply(List.scala:76)
 at scala.collection.immutable.List.apply(List.scala:76)
 at .f(<console>:12)
 at .<init>(<console>:14)
 at .<clinit>(<console>)
 at .<init>(<console>:11)
 at .<clinit>(<console>)
 at $print(<console>)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
 at java.lang.reflect.Method.invoke(Method.java:597)
 at scala.tools.nsc.interpreter.IMain$ReadEvalPrint.call(IMain.scala:704)
 at scala.tools.nsc.interpreter.IMain$Request$$anonfun$14.apply(IMain.scala:920)
 at scala.tools.nsc.interpreter.Line$$anonfun$1.apply$mcV$sp(Line.scala:43)
 at scala.tools.nsc.io.package$$anon$2.run(package.scala:25)
 at java.lang.Thread.run(Thread.java:680)

Option化

Listの範囲外の数値を指定された時にもきちんと動作させるためにはFunction1ではなくPartialFunctionを使用します。PartialFunctionはliftメソッドでOption化されたFunction1に変換されるので、これを利用します。

Option化した関数fは以下になります。

def f(a: PartialFunction[Int, String])(b: Int): Option[String] = {
  a.lift(b)
}

List(Seq)もMapもPartialFunctionなので、そのまま関数fの引数に指定することができます。

動作結果は以下のとおりです。

scala> f(l)(3)
res9: Option[String] = Some(three)

scala> f(m)(5)
res10: Option[String] = Some(five)

scala> f(l)(10)
res11: Option[String] = None

PartialFunctionの合成

PartialFunctionはorElseを使って合成するのが、よく出てくるテクニックです。

関数fの引数として、lとmをorElseで合成したものを指定すると以下のように動作します。

scala> f(l orElse m)(3)
res12: Option[String] = Some(three)

scala> f(l orElse m)(5)
res13: Option[String] = Some(five)

scala> f(l orElse m)(10)
res14: Option[String] = None

Int値が0から3の場合は変数lのListに、4と5の場合は変数mのMapが対応する値を返します。それ以外の数値の場合は対応できるPartialFunctionがないためNoneが返ります。

ノート

Map, Function1, PartialFunction, Option」のMapも、今回のListも、Function1かつPartialFunctionであるというのが関数型プログラミング的には重要なポイントです。

関数型プログラミングでは、ファンクタの対象となるA→Bの関数、モナドの対象となるA→M[B]の関数(Mはモナド)が非常に重要な意味を持ちます。この形をした関数を部品として合成していくのが関数型プログラミングの基本的な考え方になるためです。

MapやSeqはFunction1つまりA→Bの関数です。またPartialFunctionであることはliftメソッドでA→Option[B]というA→M[B]の形に持ち込めます。つまり、MapやSeq特有の機能は持ちながら、いざとなればA→BやA→M[B]の関数としても使える点がプログラミングでの選択肢を広げることにつながっているわけです。

諸元

  • Scala 2.9.2

0 件のコメント:

コメントを投稿