ScalaのPartialFunctionの話

ScalaのPartialFunctionの話

みなさん、こんにちは。ワタナベ@札幌です。
今回のテーマは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の存在を知ることで普段使っているコレクションの見方が少し変わったように感じます。それと、標準ライブラリのコードを読むことで言語仕様の素晴らしさと己の無力さを改めて実感しました。。
最後までお読みいただきありがとうございました!

採用情報

メンバーズエッジで最高のチームで最高のプロダクトを作りませんか?

最高のプロダクトをつくる 最高のチームで働く

在宅でも、地方でも、首都圏でも。多様な働き方で最高のチームをつくり、お客様のプロダクトパートナーを目指します。アジャイル開発を通じ、開発現場の第一線で活躍し続けたいエンジニアを募集しています。