ScalaのPartial Function

7399 ワード

芸術的に言えば、Scalaの中のPartial Functionは「欠けている」関数であり、深刻な偏科の学生のように、いくつかの科目に興味を持っているだけで、興味のない内容には靴を捨てている.Partial Functionでは「バイアス」で一括することはできません.そのため、複数のバイアス関数を組み合わせて、最終的には全面的にカバーする目的を達成する必要があります.だからこのPartial Functionは確かに「部分」の関数です.
FunctionとPartial Functionを比較すると、より学術的な意味の解釈は以下の通りです.
指定した入力パラメータタイプに対して、関数はそのタイプの任意の値を受け入れることができます.すなわち、1つの(Int)=>Stringの関数は、任意のInt値を受信し、文字列を返すことができる.
与えられた入力パラメータタイプに対して、バイアス関数はそのタイプの特定の値しか受け入れられません.(Int)=>Stringと定義されたバイアス関数では、すべてのInt値が入力として受け入れられない場合があります.
Scalaでは、すべてのバイアス関数のタイプがPartialFunction[-A,+B]タイプとして定義され、PartialFunction[-A,+B]はFunction 1から派生する.入力パラメータの一部のブランチのみを処理するため、isDefineAt()によって入力値が現在のバイアス関数で処理されるべきかどうかを判断します.PartialFunctionの定義は以下の通りです.
trait PartialFunction[-A, +B] extends (A => B) { self =>
  import PartialFunction._
  def isDefinedAt(x: A): Boolean
  def applyOrElse[A1 <: a="" b1="">: B](x: A1, default: A1 => B1): B1 =
    if (isDefinedAt(x)) apply(x) else default(x)
}  

バイアス関数が部分分岐のみを処理する以上,自然にモードマッチングと結びつけることができる.case文は本質的にPartialFunctionのサブクラスである.次の値を定義します.
val p:PartialFunction[Int, String] = { case 1 => "One" }

実際には、isDefineAtメソッドがこのような実装を提供するPartialFunction[Int,String]のサブクラスが作成されています.
def isDefineAt(x: Int):Boolean = x == 1

このバイアス関数をp(1)で呼び出すと,Int=>String関数を呼び出すapply()メソッドに相当し,変換後の値「one」を返す.入力されたパラメータがisDifineAtにfalseを返すと、MatchError異常が放出されます.遡及元は、ここでバイアス値を呼び出すためであり、実際にはAbstractPartialFunctionを呼び出すapply()メソッド(case文はAbstractPartialFunctionのサブクラスを継承することに相当する):
abstract class AbstractPartialFunction[@specialized(scala.Int, scala.Long, scala.Float, scala.Double, scala.AnyRef) -T1, @specialized(scala.Unit, scala.Boolean, scala.Int, scala.Float, scala.Long, scala.Double, scala.AnyRef) +R] extends Function1[T1, R] with PartialFunction[T1, R] { self =>
    def apply(x: T1): R = applyOrElse(x, PartialFunction.empty)
}

apply()メソッド内部でPartialFunctionのapplyOrElse()メソッドが呼び出されます.isDefineAt(x)がfalseに戻ると、x値がPartialFunctionに渡される.empty.このemptyは、PartialFunction[Any,Nothong]のタイプの値empty_に等しいpfは、以下のように定義される.
  private[this] val empty_pf: PartialFunction[Any, Nothing] = new PartialFunction[Any, Nothing] {
    def isDefinedAt(x: Any) = false
    def apply(x: Any) = throw new MatchError(x)
    override def orElse[A1, B1](that: PartialFunction[A1, B1]) = that
    override def andThen[C](k: Nothing => C) = this
    override val lift = (x: Any) => None
    override def runWith[U](action: Nothing => U) = constFalse
  }

これがp(2)を実行するとMatchErrorが投げ出される由来である.
なぜ偏関数を使うのですか?私個人の愚見では、粒度を再利用する問題でもある.関数式のプログラミング思想は「帰納法」ではなく「演繹法」で解決空間を求めることである.すなわち,問題をまとめて問題を分解して解決するのではなく,問題の本質を見極め,最初の操作と組合せ規則を定義し,問題に直面したとき,様々な関数を組み合わせて問題を解決することができ,これもまさに「組合せ子(combinator)」の意味である.バイアス関数はさらに,関数解空間における各分岐も分離し,組合せ可能なバイアス関数を形成する.
バイアス関数で最も一般的な組み合わせはorElse,andThen,composeである.orElseは1つまたは演算に相当し、それによって複数のバイアス関数を組み合わせると、複数のcase合成を形成するモードマッチングに相当する.すべてのバイアス関数が入力値のすべての分岐を満たすと,組み合わせて関数が形成される.たとえば、絶対値を求める演算を書くと、バイアス関数を使用できます.
val positiveNumber:PartialFunction[Int, Int] = { case x if x > 0 => x }
val zero:PartialFunction[Int, Int] = { case x if x == 0 => 0 }
val negativeNumber:PartialFunction[Int, Int] = { case x if x < 0 => -x }

def abs(x: Int): Int = {
    (positiveNumber orElse zero orElse negativeNumber)(x)
} 

orElseを使用する場合は、case文を直接組み合わせることもできます.たとえば、次のようになります.
val pf: PartialFunction[Int, String] = {
  case i if i%2 == 0 => "even"
}
val tf: (Int => String) = pf orElse { case _ => "odd" }

orElseはPartialFunctionタイプに定義されていますが、andThenはcomposeとは異なり、実際にはFunctionに定義されています.PartialFunctionはこの2つの方法を書き直しただけです.これは,関数間の組合せがandThenとcomposeを用いることができ,バイアス関数も用いることができることを意味する.この2つの方法の機能は、複数の(バイアス)関数を組み合わせて新しい関数を形成することであるが、組み合わせの順序が異なるだけで、andThenは組み合わせの1つ目であり、次いで2つ目であり、順次類推される.composeは順番が逆です.
andThen組合せバイアス関数を用いて,設計の本質はPipe−and−Filterモードに近く,各バイアス関数はFilterであると理解できる.これらのバイアス関数を組み合わせてパイプを形成するためには、結合されたバイアス関数の入力値と出力値が直列接続可能であることが要求される.すなわち、前のバイアス関数の出力値が次のバイアス関数の入力値となる.orElseと比較すると、orElseでは、組み合わせられたすべてのバイアス関数が同じタイプのバイアス関数定義である必要があります.たとえば、Int=>String、String=>CustomizedClassなどです.
PartialFunctionでandThenメソッドはAndThenという偏関数を返します.
trait PartialFunction[-A, +B] extends (A => B) {
  override def andThen[C](k: B => C): PartialFunction[A, C] =
    new AndThen[A, B, C] (this, k)
}
object PartialFunction {
  private class AndThen[-A, B, +C] (pf: PartialFunction[A, B], k: B => C) extends PartialFunction[A, C] {
    def isDefinedAt(x: A) = pf.isDefinedAt(x)

    def apply(x: A): C = k(pf(x))

    override def applyOrElse[A1 <: a="" c1="">: C](x: A1, default: A1 => C1): C1 = {
      val z = pf.applyOrElse(x, checkFallback[B])
      if (!fallbackOccurred(z)) k(z) else default(x)
    }
  }
}  

なお、andThenが受信するパラメータはk:B=>Cであり、バイアス関数タイプではなく関数タイプである.もちろん、バイアス関数は自己関数を継承するため、バイアス関数を組み合わせることもできます.andThenがバイアス関数を結合している場合、入力パラメータはすべての結合に関与するバイアス関数を満たす必要があります.そうしないと、MatchErrorエラーが放出されます.たとえば、文字列の数字を対応する英語の単語に置き換える関数を作成すると、次のように実現できます.
val p1:PartialFunction[String, String] = { case s if s.contains("1") => s.replace("1", "one") }
val p2:PartialFunction[String, String] = { case s if s.contains("2") => s.replace("2", "two") }
val p = p1 andThen p2

p(「123」)を呼び出すと結果は「onetwo 3」となるが、p(「13」)に入ると、p 2バイアス関数のisDefineAtがfalseを返すため、MatchErrorエラーが投げ出される.
バイアス関数は多くのシーンで使用できます.例えばorElseのような意味を利用して、DSLスタイルのコードを書き、より柔軟で読み取りやすいようにすることができます.「DSL in Action」という本では、orElseを使って金融業界のニーズを処理しています.
val forHKG:PartialFunction[Market, List[TaxFee]] = ...
val forSGP:PartialFunction[Market, List[TaxFee]] = ...
val forAll:PartialFunction[Market, List[TaxFee]] = ...

def forTrade(trade: Trade): List[TaxFee] = 
    (forHKG orElse forSGP orElse forAll)(trade.market)

バイアス関数の開放性を効果的に利用することもでき、APIの呼び出し者が特定のニーズに応じてシーンに基づいて自分のcase文を伝達することができる.例えば[TwitterのEffective Scale](http://twitter.github.io/effectivescala/#Functionalprogramming-Partial functions)が示す例:
trait Publisher[T] {
  def subscribe(f: PartialFunction[T, Unit])
}

val publisher: Publisher[Int] = ...
publisher.subscribe {
  case i if isPrime(i) => println("found prime", i)
  case i if i%2 == 0 => count += 2
  /* ignore the rest */
}

AKKAのActorに定義されるreceive()メソッドもバイアス関数です.
trait Actor {
    type Receive = Actor.Receive
    def receive: Actor.Receive
}
object Actor {
  type Receive = PartialFunction[Any, Unit]
}

バイアス関数は自己関数を継承するため、1つの方法が受信関数を要求する場合、バイアス関数も受信することができる.たとえば、map、filterなどの方法をよく使用すると、バイアス関数を受信できます.
val sample = 1 to 10
sample map {
    case x if x % 2 == 0 => x + " is even"
    case x if x % 2 == 1 => x + " is odd"
}

TwitterのEffetive Scalaでは、mapを使用したエンコードスタイルの提案があります.
//avoid
list map { item =>
  item match {
    case Some(x) => x
    case None => default
  }
}
//recommend
list map {
  case Some(x) => x
  case None => default
}

本質的には、このlistのタイプがList[Option[String]]であると仮定すると、前者がmapに渡されるのは、Option[String]=>Stringのような形をした関数であり、後者はcase文によってPartialFunction[Option[String],Stringを作成したインスタンスがmapに渡される.