読者です 読者をやめる 読者になる 読者になる

( ꒪⌓꒪) ゆるよろ日記

( ゚∀゚)o彡°オパーイ!オパーイ! ( ;゚皿゚)ノシΣ フィンギィィーーッ!!!

ScalaのPartialFunctionが便利ですよ

Scalaには、PartialFunctionというものがあります。


直訳すると部分関数ですが、これはなにかっていうと「特定の引数に対しては結果を返すけど、結果を返せない引数もあるような中途半端な関数」です。

どうやって使うのん?

まぁ、ちょっと例を見てましょうや。PartialFunctionであるfooPfは、引数が"foo"だったら"bar"を返して、"foo"以外は知らんというてきとーな関数です。

scala> val fooPf:PartialFunction[String,String] = { case "foo" => "bar" }
fooPf: PartialFunction[String,String] = <function1>

scala> fooPf("foo")
res5: String = bar

scala> fooPf("hoge")
scala.MatchError: hoge
	at $anonfun$1.apply(<console>:5)
	at $anonfun$1.apply(<console>:5)


PartialFunctionは、Function1のサブトレイトで、ようは引数をひとつもらう関数です。fooPfは、String型をもらってString型を返すので、PartialFunction[String,String]ですよ。


PartialFunctionを定義する際には、match式の中のcase句の集合として定義できます。このような書き方は実はシンタックスシュガーなんですけどね。

引数に対して結果を返すかisDefinedAtで調べる

で、fooPf("foo")と引数に"foo"を与えるとちゃんと"bar"が返りますが、"hoge"とかで呼び出すとMatchErorrをなげるとんでもないヤツです。


これだけだと使いにくくてしょうがないですが、PartialFunctionには事前に引数に対して結果を返すか調べる関数が定義されています。isDefinedAt[A]です。

scala> fooPf.isDefinedAt("foo")
res6: Boolean = true

scala> fooPf.isDefinedAt("hoge")
res7: Boolean = false


このようにして、PartialFunctionが結果を返すかは事前にしることができます。

orElseでPartialFunctionを合成する

PartialFunctionは、orElseという関数で他のPartialFunctionと合成できます。以下のような引数が"baz"だったら"hoge"を返すPartialFunctionとfooPfをorElseで合成すると、引数が"foo"または"baz"だったら結果を返す新しいPartialFunctionであるfooOrBazPFが生成されます。

scala> val bazPf:PartialFunction[String,String] = { case "baz" => "hoge" }
bazPf: PartialFunction[String,String] = <function1>

scala> val fooOrBazPF = fooPf orElse bazPf
fooOrBazPF: PartialFunction[String,String] = <function1>

scala> fooOrBazPF( "foo")
res8: String = bar

scala> fooOrBazPF( "baz")
res9: String = hoge

scala> fooOrBazPF( "aaa")
scala.MatchError: aaa
	at $anonfun$1.apply(<console>:5)
	at $anonfun$1.apply(<console>:5)

まぁ、ぶっちゃけて感覚的にいうと、PartialFunctionってのはmatch式のパターンを部分的にオブジェクトとして取り扱うことができるってことです。いろいろな条件のPartialFunctionを作っておいて、orElseで合成しながら条件に応じたmatch式をランタイムにくみ上げることができますよ。


Liftでも、URLのmappingあたりでPartialFunctionが使われてます。

コレクションとPartialFunction

で、PartialFunctionの一番の使いどころは、コレクションに対するMap操作でしょうか。


まず、引数のStringがnullまたは空文字だったらNoneでそれ以外はSome[String]を返すPartialFunctionをこんな風に定義しておきます。

scala> val pf:PartialFunction[String,Option[String]] = {
     |   case null => None
     |   case "" => None
     |   case s => Some(s)
     | }
pf: PartialFunction[String,Option[String]] = <function1>

scala> pf( null)
res11: Option[String] = None

scala> pf( "")  
res12: Option[String] = None

scala> pf( "hogehoge")
res13: Option[String] = Some(hogehoge)


で、Seq[String]であるコレクションから、nullと空文字の要素を削除したいわけです。
こんな風に使います。

scala> val list = Seq( "foo",null,"bar","","","baz")
list: Seq[java.lang.String] = List(foo, null, bar, , , baz)

scala> list filter{ pf.isDefinedAt } map{ pf }
res14: Seq[Option[String]] = List(Some(foo), None, Some(bar), None, None, Some(baz))

scala> list filter{ pf.isDefinedAt } map{ pf } flatten
res15: Seq[String] = List(foo, bar, baz)

このように、Seq#filter( f: A => Boolean )でfilterする条件として、PartialFunciton#isDefinedAtを呼び出すようにして、PartailFunctionの条件に一致するものだけ、Seq#map[B]( f: A => B )の引数にPartailFunctionを渡すことで、条件に応じてmapされたSeqができます。


で、結果はSeq[Option[String]]なので、Seq#flattenを呼び出せばNoneが消えるという訳です。


さて、Scala2.8からは引数にPartailFunctionをもらって条件に応じた要素だけmapするTraversableLike#collect[B, That](pf: PartialFunction[A, B]): Thatという関数があります。


さきほどの処理は、もう少し簡単にするとこんな感じです。

scala> val pf:PartialFunction[String,String] = { case s if s != null || s != "" => s } 
pf: PartialFunction[String,String] = <function1>

scala> list.collect( pf )
res16: Seq[String] = List(foo, bar, baz)

scala> list collect { case s if s != null || s != "" => s } 
res17: Seq[java.lang.String] = List(foo, bar, baz)

おまけ。カッコイイ書き方

水島さんに教えてもらったんですが、以下のようなtype aliasを定義しておくと、"pf:String --> Option[String]"みたいにPartailFunctionがかっこよく書けますよ!

scala> type -->[A,B] = PartialFunction[A,B]
defined type alias $minus$minus$greater

scala> val pf:String --> Option[String] = { case s if s == null || s == "" => None; case s => Some(s) }
pf: -->[String,Option[String]] = <function1>

scala> list collect pf flatten                                                                  res20: Seq[String] = List(foo, bar, baz)