みなさん、こんにちは。ワタナベ@札幌です。
今回のテーマはPartialFunctionです。
Scalaのコードを書いていてPartialFunctionをそのまま使うことはなかなか稀ですが、実は要所で私たちを支えてくれています。そんな縁の下の力持ちであるPartialFunctionに日頃の感謝を込めて本稿をしたためました。
PartialFunctionはどこで使われている?
以下が定義されているとします。
- 学生の名簿(学籍番号と氏名の対)をMap[Int, String]で定義
- 何らかのグループに所属している学生(学籍番号)の集合をList[Int]で定義
val students: Map[Int, String] = Map(1 -> "Aoki", 2 -> "Bandou", 3 -> "Chiba", 4 -> "Doi") val group: List[Int] = List(1,3)
このとき、Listのcollectメソッドでグループに所属しているメンバーの氏名を取得することができます。
group collect students // Aoki, Chiba
collectは便利なメソッドなので使ったことがあるの方も多いのではないでしょうか。上記のコードにはPartialFunctionは登場しませんが、collectの中身(於 2.13.3)をのぞいてみると…
final override def collect[B](pf: PartialFunction[A, B]): List[B] = { if (this eq Nil) Nil else { var rest = this var h: ::[B] = null var x: Any = null // Special case for first element while (h eq null) { x = pf.applyOrElse(rest.head, List.partialNotApplied) if (x.asInstanceOf[AnyRef] ne List.partialNotApplied) h = new ::(x.asInstanceOf[B], Nil) rest = rest.tail if (rest eq Nil) return if (h eq null) Nil else h } var t = h // Remaining elements while (rest ne Nil) { x = pf.applyOrElse(rest.head, List.partialNotApplied) if (x.asInstanceOf[AnyRef] ne List.partialNotApplied) { val nx = new ::(x.asInstanceOf[B], Nil) t.next = nx t = nx } rest = rest.tail } releaseFence() h } }
collectの引数はPartialFunctionであることがわかります。students
はPartialFunctionとして渡されていたのですね!(ちなみに、コード中の var h: ::[B] = null
の::
はListのサブクラスです)
collectの中で使われているPartialFunctionのメソッドはapplyOrElseです。pf.applyOrElse(rest.head, List.partialNotApplied)
この部分です。
中身を見てみましょう!
def applyOrElse[A1 <: A, B1 >: B](x: A1, default: A1 => B1): B1 = if (isDefinedAt(x)) apply(x) else default(x)
xを引数にとる部分関数が定義されていれば(Mapの場合はisDefinedAtはcontains(key)
で実装されています)その関数を適用(apply)し、定義されていなければ引数で受け取った関数default(Listのcollectの場合は、List.partialNotApplied
が渡されています)を適用して返しています。
グループに所属している学生の氏名を抽出する例だと、名簿に学籍番号が存在していれば氏名を返す、存在しない場合は何も返さない(=結果に含めない)という動作をします。以下のコードのようにグループの学籍番号リストに名簿に存在しない番号が含まれていても正常に動作します(存在しない学籍番号は結果に含まれない)。
val students: Map[Int, String] = Map(1 -> "Aoki", 2 -> "Bandou", 3 -> "Chiba", 4 -> "Doi") val group: List[Int] = List(1, 3, 5) //5は学生の名簿に存在しない group collect students // Aoki, Chiba
Optionのcollect
もう一つPartialFunctionを引数にとるメソッドを見てみましょう。
Optionのcollectです。
PartialFunctionに関数が定義されていればSomeで包んで返して、定義されていなければNoneを返す動作をします。「もし〜だったら〜を返す」のように一見して分岐処理を書きたくなる動作を簡潔に書けるのが爽快ですね!これもPartialFunctionのおかげです。
Some(1) collect students // Some(Aoki) Some(5) collect students // None
PartialFunctionの親戚たち
では続いて、PartialFunctionを継承・ミックスインしているクラスを見てみましょう。
Mapの親トレイトのMapOpsや…
trait MapOps[K, +V, +CC[_, _] <: IterableOps[_, AnyConstr, _], +C] extends IterableOps[(K, V), Iterable, C] with PartialFunction[K, V] // <-ココ
Seqにもミックスインされています。
trait Seq[+A] extends Iterable[A] with PartialFunction[Int, A] // <-ココ with SeqOps[A, Seq, Seq[A]] with IterableFactoryDefaults[A, Seq] with Equals
なので、
val students: Map[Int, String] = Map(1 -> "Aoki", 2 -> "Bandou", 3 -> "Chiba", 4 -> "Doi")
を
val students: Seq[String] = Seq("", "Aoki", "Bandou", "Chiba", "Doi")
としても、collectの引数に取ることができます!
PartialFunctionの中身
PartialFunctionの定義や実装されているメソッドを見てみましょう!
まずは定義ですが、
trait PartialFunction[-A, +B] extends (A) => B
(A) => B(Function1)を継承しています。
docの中に
does not necessarily include all values of type A.
と書かれているように、すべてのAに対してBを返すような関数でなくてもOKとのことです。(そのかわり、Mix-inするクラスはdef isDefinedAt(x: A): Boolean
を実装する必要があります)
関数合成
関数を合成するためのメソッドが定義されています。
orElse
部分関数を並列に合成します。部分関数の「部分」の範囲を大きくするイメージです。下記の例ではorElse
で2つのMap(PartialFunction)を合成することによって学生の名簿を拡張しています。応用して色々な場面で利用できそうな予感がします。
val students: Map[Int, String] = Map(1 -> "Aoki", 2 -> "Bandou", 3 -> "Chiba", 4 -> "Doi") val studentsOther: Map[Int, String] = Map(5 -> "Endo", 6 -> "Fujita", 7 -> "Gouda") List(1,3,5) collect(students orElse studentsOther) // List(1,3,5)はグループに所属する学籍番号の集合
andThen
andThenは部分関数を直列に合成します。例として、文字列を引数にとって「Hello」を先頭に付加した文字列を返すhelloを合成してみます。(ちなみにFunction1を引数にとるandThenはFunction1の実装をオーバーライドし、引数の型によって処理を分岐させています。)
val hello: PartialFunction[String, String] = x => "Hello " + x val students: Map[Int, String] = Map(1 -> "Aoki", 2 -> "Bandou", 3 -> "Chiba", 4 -> "Doi") group1 collect (students andThen hello) // Hello Aoki, Hello Chiba
さいごに
PartialFunctionの存在を知ることで普段使っているコレクションの見方が少し変わったように感じます。それと、標準ライブラリのコードを読むことで言語仕様の素晴らしさと己の無力さを改めて実感しました。。
最後までお読みいただきありがとうございました!