2013年5月2日木曜日

[Scala] PartialFunctionのリテラルって、caseから始めなくてもいいみたい

Scalaなのです。

もともと関数型言語が好きで、でもSI業界(のさらに自分がいるところ)はまだまだJavaだろうなー、でもやっぱ関数型言語使いたいなー、
と思ったので、Javaの資産が無駄なくご利用できるScalaをとりあえず勉強してみるか、と思ったのです。
これを実際に使うには、自分自身の働きかけだけではどうにもならない大いなる力がありますし、Scalaは結構変態仕様な部分もあるのでますますアレですが、
まあ関数型触っていて楽しいのでよし。

で、いきなりPartialFunctionの話です。
PartialFunctionがなんであるかは、ググればすぐにわかります。
ここ(ScalaのPartialFunctionが便利ですよ@ゆるよろ日記)とか。
しかし、それでは不親切すぎるので少しだけ。

「1引数をとる関数の一種なんだけど、引数の値によっては定義されていない(結果を返せない)ものがある関数」です。

プログラミング言語とはちょっと違いますが、
f(x) = 1 / x (x ≠ 0)
もPartialFunctionです。
xが0のところでは、定義されないでしょ。

たとえば上の関数をScalaで書いてみます。

val reciprocal = (x: Int) => 1 / x
println(reciprocal(0))
意味のある結果を返そうと思ったら、xをDoubleにするべきですが、ここは説明のため、Intのままで。

さて、こうして定義した"reciprocal"、0を代入すると、java.lang.ArithmeticException: / by zeroとなります。
0では割れませんエラー。お約束ですよね。

しかしこれではただの「場合によっては例外を投げる、普通の関数」です。

PartialFunctionには、isDefinedAtというメソッドがあります。
これがtrueになるところでは、定義されている、falseになるところでは定義されていない、という意味です。
そして、PartialFunctionのリテラルは、"case"から書き始める、つまり、パターンマッチから書き始めます。これはシンタックスシュガーだそうです。

val reciprocal: PartialFunction[Int, Int] = {
  case x if x != 0 => 1 / x
}
println(reciprocal.isDefinedAt(0))
println(reciprocal(0))
こうすると、パターンマッチに当てはまる値(つまり0以外)のところではisDefinedはtrue、そうでないところではfalseを返します。
上のコードでは、println(reciprocal.isDefinedAt(0))は"false"を表示します。
しかし、falseを返す値でもPartialFunctionは実行(apply)可能で、当然といえば当然ですが、3行目のコードはMatchErrorを投げます。

さて、こいつのつかいどころはどこか?

collectです。

collectは、Listなどのコレクションで使える、filterとmapが合わさったような機能。
PartialFunction pfを引数にとり、

list.withFilter(pf.isDefined).map(pf)
みたいなことをします。つまり、isDefinedでfalseを返す値を捨て、残りの値を変換してコレクションとして返す機能です。

ちょっと使ってみましょう。
Scalaのソースコードから1行コメントを抜き出し、TODOとFIXMEという文字列がついたものはちょっぴり強調する、という機能です。

object PFTest extends App {

  // 自分自身を読み込む
  val source = Source.fromFile("""このファイル自身のフルパス""")

  val todoPattern = "// TODO (.*)".r
  val fixmePattern = "// FIXME (.*)".r
  val commentPattern = "//(.*)".r

  // TODO このパターンを後で書き換えます
  source.getLines.map(_.trim()).collect {
    case todoPattern(contents) => "[ちゃんとやっとけよ]: " + contents
    case fixmePattern(contents) => "[ここほっといたらマジヤバイ]: " + contents
    // FIXME こんな文字列で大丈夫か?
    case commentPattern(contents) => "何かコメントがあったみたい: " + (if (contents.length() < 10) contents else contents.substring(0, 10))
  }.foreach(println)

  // クローズを忘れずに
  source.close()

}
ここでは、自分自身のソースコードを読み込んでいます。結果はこうなります。
何かコメントがあったみたい: 自分自身を読み込む
[ちゃんとやっとけよ]: このパターンを後で書き換えます
[ここほっといたらマジヤバイ]: こんな文字列で大丈夫か?
何かコメントがあったみたい: クローズを忘れずに
このコードでは、各行がどんな文字列か判別するため、正規表現のパターンマッチを使いました。

では、もっと簡単なケースを考えてみましょう。
1行コメントを抜き出して表示するだけでいい、となったら、どうでしょう。
もちろん、前のように正規表現を使ってもいいですし、withFilterとmapだけで簡潔に書けますが、こんな風にもかけるのです。

object PFTest extends App {

  // 自分自身を読み込む
  val source = Source.fromFile("""このファイル自身のフルパス""")

  // 1行コメントのみを取り出して、その内容を表示する
  source.getLines.map(_.trim()).withFilter(_.length() > 2).collect { line =>
    line.substring(0, 2) match {
      case "//" => line.substring(2)
    }
  }.foreach(println)

  // クローズを忘れずに
  source.close()
}
結果はこうなります。
自分自身を読み込む
1行コメントのみを取り出して、その内容を表示する
クローズを忘れずに
えー、お分かりになるでしょうか。
collectの引数はPartialFunctionですが、"case"から始まっていません。普通の関数リテラルっぽく始まりまり、最後にmatchを使っています。

普通の関数リテラルでも大丈夫なんじゃないの?と思って、matchじゃなくてifを使ったら、やっぱりコンパイルエラー。
どうやら、関数本体がmatchである場合のみ、PartialFunctionと認めてくれるようなのです。

いきなり"case"でなくてもよい、となれば、collectの使い勝手も良くなるというもの。
皆様、お試しあれ。