【scala原理系列】scala PartialFunction偏函数原理示例源码分析

本文对Scala中的偏函数进行了深入分析。介绍了偏函数原理,它只对输入域的子集定义。还总结了偏函数的多种方法,如isDefinedAt、apply等,并给出代码示例展示其使用方式和效果,有助于理解偏函数在实际开发中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

scala PartialFunction偏函数原理示例源码分析

原理

Scala中的PartialFunction(偏函数)是一种特殊类型的函数,它只对输入域的某个子集定义。换句话说,它只在满足特定条件的输入值上进行操作,而在其他情况下则不进行处理。

PartialFunction是一个特质(trait),定义如下:

trait PartialFunction[-A, +B] extends (A) => B {
  def isDefinedAt(x: A): Boolean
  def apply(x: A): B
  ...
}

其中,-A表示输入类型,+B表示输出类型。isDefinedAt方法用于判断给定的输入值是否在该偏函数的定义域内,apply方法则定义了实际的操作逻辑。

偏函数可以通过case语句块来定义,例如:

val divideByZero: PartialFunction[Int, Int] = {
  case x if x != 0 => 10 / x
}

在上述例子中,divideByZero是一个偏函数,它接收一个Int类型的参数,并返回一个Int类型的结果。该偏函数定义了当输入值不为零时,计算 10 / x 的操作。

方法总结

函数名字描述参数返回值
isDefinedAt(x: A): Boolean检查给定的值是否在函数的定义范围内。x - 要测试的值。如果x在函数的定义范围内,则返回true,否则返回false
apply(x: A): B将给定的参数应用于函数,并返回结果。x - 要应用函数的参数。函数应用后的结果。
orElse(that: PartialFunction[A1, B1]): PartialFunction[A1, B1]将当前偏函数与另一个偏函数组合起来,创建一个新的偏函数,该函数的定义范围为两个偏函数的定义范围的并集。that - 另一个偏函数。组合后的新偏函数。
andThen[C](k: B => C): PartialFunction[A, C]将当前偏函数的结果传递给转换函数,生成一个新的偏函数。k - 结果转换函数。转换后的新偏函数。
lift: A => Option[B]将偏函数转换为普通函数,返回Option类型的结果。如果输入在偏函数的定义范围内,则返回Some,否则返回None
applyOrElse[A1 <: A, B1 >: B](x: A1, default: A1 => B1): B1根据输入是否在偏函数的定义范围内,选择调用偏函数的apply方法或者调用默认的回调函数。x - 函数参数,default - 默认回调函数。如果输入在偏函数的定义范围内,则返回偏函数的结果;否则返回回调函数的结果。
runWith[U](action: B => U): A => Boolean将偏函数与动作函数组合起来,仅运行动作函数来处理偏函数的结果,而忽略返回值。action - 动作函数。一个新的函数,该函数接受参数并返回布尔值,表示偏函数是否成功执行了动作函数。
cond[T](x: T)(pf: PartialFunction[T, Boolean]): Boolean根据给定的值和偏函数,判断值是否在偏函数的定义范围内,并且偏函数的结果为truex - 要测试的值,pf - 偏函数。如果值在偏函数的定义范围内,并且偏函数的结果为true,则返回true,否则返回false
condOpt[T,U](x: T)(pf: PartialFunction[T, U]): Option[U]将偏函数转换为普通函数,根据给定的值返回Option类型的结果。如果值在偏函数的定义范围内,则返回Some,否则返回None
empty[A, B]: PartialFunction[A, B]一个空的偏函数,没有定义域。当尝试调用该偏函数时,会抛出MatchError异常。

示例

以下是每种方法的代码示例及其说明:

  1. isDefinedAt(x: A): Boolean
val divideByTwo: PartialFunction[Int, Int] = {
  case x if x % 2 == 0 => x / 2
}

println(divideByTwo.isDefinedAt(4)) // 输出: true
println(divideByTwo.isDefinedAt(5)) // 输出: false

在这个示例中,isDefinedAt方法用于检查给定的值是否在函数的定义范围内。对于偏函数divideByTwo,它只对偶数进行定义,因此isDefinedAt(4)返回true,而isDefinedAt(5)返回false

  1. apply(x: A): B
val divideByTwo: PartialFunction[Int, Int] = {
  case x if x % 2 == 0 => x / 2
}

println(divideByTwo(4)) // 输出: 2

在这个示例中,apply方法用于将给定的参数应用于函数,并返回结果。对于偏函数divideByTwo,如果输入的值是偶数,则返回其除以2的结果。

  1. orElse(that: PartialFunction[A1, B1]): PartialFunction[A1, B1]
val isEven: PartialFunction[Int, String] = {
  case x if x % 2 == 0 => s"$x is even"
}

val isOdd: PartialFunction[Int, String] = {
  case x if x % 2 != 0 => s"$x is odd"
}

val numbers: PartialFunction[Int, String] = isEven.orElse(isOdd)

println(numbers(4)) // 输出: 4 is even
println(numbers(5)) // 输出: 5 is odd

在这个示例中,orElse方法用于将两个偏函数组合起来,创建一个新的偏函数。isEven偏函数定义了对偶数的处理逻辑,而isOdd偏函数定义了对奇数的处理逻辑。通过使用orElse方法,可以创建一个新的偏函数numbers,该函数对输入值既可以执行isEven的逻辑,也可以执行isOdd的逻辑。

  1. andThen[C](k: B => C): PartialFunction[A, C]
val addOne: PartialFunction[Int, Int] = {
  case x => x + 1
}

val multiplyByTwo: PartialFunction[Int, Int] = {
  case x => x * 2
}

val operation: PartialFunction[Int, String] = addOne.andThen(result => s"The result is $result").andThen(_ + "!")

println(operation(3)) // 输出: The result is 8!

在这个示例中,andThen方法用于将当前偏函数的结果传递给转换函数,生成一个新的偏函数。addOne偏函数将输入值加一,multiplyByTwo偏函数将输入值乘以二。通过使用andThen方法,我们将新的转换函数应用于结果,得到一个新的偏函数operation,它将输入值加一并转换为字符串形式,并在末尾添加感叹号。

  1. lift: A => Option[B]
val divideByTwo: PartialFunction[Int, Int] = {
  case x if x % 2 == 0 => x / 2
}

val liftedFunction: Int => Option[Int] = divideByTwo.lift

println(liftedFunction(4)) // 输出: Some(2)
println(liftedFunction(5)) // 输出: None

在这个示例中,lift方法用于将偏函数转换为普通函数,返回Option类型的结果。对于偏函数divideByTwo,如果输入值是偶数,则返回Some包装的除以2的结果;否则返回None

  1. applyOrElse[A1 <: A, B1 >: B](x: A1, default: A1 => B1): B1
val divideByTwo: PartialFunction[Int, Int] = {
  case x if x % 2 == 0 => x / 2
}

val result1 = divideByTwo.applyOrElse(4, (x: Int) => x * 10)
val result2 = divideByTwo.applyOrElse(5, (x: Int) => x * 10)

println(result1) // 输出: 2
println(result2) // 输出: 50

在这个示例中,applyOrElse方法根据输入是否在偏函数的定义范围内,选择调用偏函数的apply方法或者调用默认的回调函数。对于偏函数divideByTwo,如果输入值是偶数,则返回其除以2的结果;否则使用默认的回调函数,将输入值乘以10。

  1. runWith[U](action: B => U): A => Boolean
val printEven: PartialFunction[Int, Unit] = {
  case x if x % 2 == 0 => println(s"$x is even")
}

val checkAndPrint: Int => Boolean = printEven.runWith(result => {
  println("Result received: " + result)
})

println(checkAndPrint(4)) // 输出:
                          // 4 is even
                          // Result received: ()
                          // true

println(checkAndPrint(5)) // 输出: false

在这个示例中,runWith方法将偏函数与动作函数组合起来,仅运行动作函数来处理偏函数的结果,而忽略返回值。对于偏函数printEven,如果输入值是偶数,则打印出该值是偶数;然后使用runWith方法将其与一个动作函数组合起来,该动作函数打印出接收到的结果。checkAndPrint函数被创建为一个新的函数,用于检查输入值是否是偶数并打印相应的消息。

  1. cond[T](x: T)(pf: PartialFunction[T, Boolean]): Boolean
val isEven: PartialFunction[Int, Boolean] = {
  case x if x % 2 == 0 => true
}

val isOdd: PartialFunction[Int, Boolean] = {
  case x if x % 2 != 0 => true
}

val result1 = PartialFunction.cond(4)(isEven)
val result2 = PartialFunction.cond(5)(isEven)

println(result1) // 输出: true
println(result2) // 输出: false

在这个示例中,cond方法根据给定的值和偏函数,判断值是否在偏函数的定义范围内,并且偏函数的结果为true。对于偏函数isEven,如果输入值是偶数,则返回true;对于偏函数isOdd,如果输入值是奇数,则返回true。通过使用cond方法,我们可以方便地检查给定的值是否满足特定的条件。

  1. condOpt[T,U](x: T)(pf: PartialFunction[T, U]): Option[U]
val divideByTwo: PartialFunction[Int, Int] = {
  case x if x % 2 == 0 => x / 2
}

val result1 = PartialFunction.condOpt(4)(divideByTwo)
val result2 = PartialFunction.condOpt(5)(divideByTwo)

println(result1) // 输出: Some(2)
println(result2) // 输出: None

在这个示例中,condOpt方法将偏函数转换为普通函数,根据给定的值返回Option类型的结果。对于偏函数divideByTwo,如果输入值是偶数,则返回Some包装的除以2的结果;否则返回None。通过使用condOpt方法,我们可以更方便地处理只对特定输入进行定义的情况,并得到相应的结果。

以上是每种方法的代码示例及其说明,展示了PartialFunction不同方法的使用方式和效果。这些示例可以帮助更好地理解偏函数在实际开发中的应用场景和用法。

中文注释

以下是中文注释版本的PartialFunction源码:

/**
 * 偏函数(Partial Function)是一种输入类型为 A、输出类型为 B 的一元函数,
 * 其定义域不一定包含类型 A 的所有值。isDefinedAt 方法可以动态地测试一个值是否在函数的定义域内。
 *
 * 即使对于某个 a: A,isDefinedAt 返回 true,调用 apply(a) 仍然可能抛出异常,
 * 因此下面的代码是合法的:
 *
 * {{{
 * val f: PartialFunction[Int, Any] = { case _ => 1/0 }
 * }}}
 * 
 * 调用者有责任在调用 apply 之前先调用 isDefinedAt,因为如果 isDefinedAt 返回 false,
 * 并不保证 apply 抛出异常来表示错误条件。如果没有抛出异常,则计算可能会得到任意值。
 *
 * PartialFunction 和 [[scala.Function1]] 的主要区别在于偏函数的使用者可以选择对其定义域之外的输入执行其他操作。例如:
 *
 * {{{
 * val sample = 1 to 10
 * val isEven: PartialFunction[Int, String] = {
 *   case x if x % 2 == 0 => x+" 是偶数"
 * }
 *
 * // collect 方法可以使用 isDefinedAt 来选择要收集的成员
 * val evenNumbers = sample collect isEven
 *
 * val isOdd: PartialFunction[Int, String] = {
 *   case x if x % 2 == 1 => x+" 是奇数"
 * }
 *
 * // orElse 方法允许链接另一个偏函数来处理声明域之外的输入
 * val numbers = sample map (isEven orElse isOdd)
 * }}}
 *
 * 作者:Martin Odersky, Pavel Pavlov, Adriaan Moors
 * 版本:1.0,16/07/2003
 */
trait PartialFunction[-A, +B] extends (A => B) { self =>
  import PartialFunction._

  /**
   * 检查值是否在函数的定义域内。
   *
   * @param x 要测试的值
   * @return 如果 x 在该函数的定义域内,则返回 true,否则返回 false
   */
  def isDefinedAt(x: A): Boolean

  /**
   * 将当前偏函数与另一个偏函数组合起来,
   * 创建一个新的偏函数,其定义域为两个偏函数的并集。
   *
   * @param that 其他偏函数
   * @tparam A1 新偏函数的参数类型(是当前偏函数参数类型的子类型)
   * @tparam B1 新偏函数的结果类型(是当前偏函数结果类型的超类型)
   * @return 一个新的偏函数,它的定义域是当前偏函数和传入的偏函数的并集。
   *         对于属于当前偏函数定义域的输入,返回 this(x);
   *         对于不属于当前偏函数定义域的输入,返回 that(x)。
   */
  def orElse[A1 <: A, B1 >: B](that: PartialFunction[A1, B1]): PartialFunction[A1, B1] =
    new OrElse[A1, B1](this, that)
  //TODO: 为什么不使用重载来定义 orElse(that: F1): F1?

  /**
   * 将当前偏函数的结果应用于转换函数,
   * 生成一个新的偏函数。
   *
   * @param k 转换函数
   * @tparam C 转换函数的结果类型
   * @return 具有与当前偏函数相同定义域的新偏函数,将参数 x 映射到 k(this(x))
   */
  override def andThen[C](k: B => C): PartialFunction[A, C] =
    new AndThen[A, B, C](this, k)

  /**
   * 将偏函数转换为返回 Option 结果的普通函数。
   * @see Function.unlift
   * @return 一个函数,它接受一个参数 x,
   *         如果该偏函数在 x 处定义,则返回 Some(this(x));
   *         否则返回 None。
   */
  def lift: A => Option[B] = new Lifted(this)

  /**
   * 当函数包含输入值时,将此偏函数应用于给定的参数。
   * 在此偏函数未定义的情况下,应用其他回退函数。
   *
   * 注意,表达式 `pf.applyOrElse(x, default)` 等效于
   * {{{ if(pf isDefinedAt x) pf(x) else default(x) }}}
   * 除了 applyOrElse 方法可以更高效地实现之外。
   * 对于所有偏函数字面量,编译器会生成一个 applyOrElse 实现,
   * 它避免了模式匹配和守卫条件的二次评估。
   * 这使得 applyOrElse 成为许多操作和场景的高效实现基础,例如:
   *
   * - 将偏函数组合成 orElse/andThen 链不会导致过多的 apply/isDefinedAt 评估
   * - lift 和 unlift 在每次调用时不会两次计算源函数
   * - runWith 允许将偏函数与有条件地应用的动作以有效的命令式风格组合
   *
   * 对于具有非平凡 isDefinedAt 方法的非字面量偏函数类,建议重写 applyOrElse,使用自定义实现来避免二次 isDefinedAt 评估。
   * 这可能会提供更好的性能和对副作用的更可预测行为。
   *
   * @param x       函数参数
   * @param default 回退函数
   * @return 函数结果或回退函数应用的结果
   * @since 2.10
   */
  def applyOrElse[A1 <: A, B1 >: B](x: A1, default: A1 => B1): B1 =
    if (isDefinedAt(x)) apply(x) else default(x)

  /**
   * 将此偏函数与一个 action 函数组合起来,
   * 该函数在此偏函数的结果上应用。
   * action 函数仅用于副作用,其结果被忽略。
   *
   * 注意,表达式 `pf.runWith(action)(x)` 等效于
   * {{{ if(pf isDefinedAt x) { action(pf(x)); true } else false }}}
   * 除了 runWith 是通过 applyOrElse 实现的,因此可能更高效。
   * 使用 runWith 可以避免针对偏函数字面量的模式匹配和守卫条件的二次评估。
   * @see `applyOrElse`
   *
   * @param action 动作函数
   * @return 一个函数,将参数 x 映射到 isDefinedAt(x)。
   *         结果函数在 this(x) 已定义时运行 action(this(x))。
   * @since 2.10
   */
  def runWith[U](action: B => U): A => Boolean = { x =>
    val z = applyOrElse(x, checkFallback[B])
    if (!fallbackOccurred(z)) { action(z); true } else false
  }
}

/**
 * PartialFunction 对象提供了一些方便的操作,利用了偏函数中额外的信息。
 * 示例:
 * {{{
 * import PartialFunction._
 *
 * def strangeConditional(other: Any): Boolean = cond(other) {
 *   case x: String if x == "abc" || x == "def" => true
 *   case x: Int => true
 * }
 * def onlyInt(v: Any): Option[Int] = condOpt(v) { case x: Int => x }
 * }}}
 *
 * 作者:Paul Phillips
 * 版本:2.8
 */
object PartialFunction {
  /** `PartialFunction#orElse` 方法生成的复合函数 */
  private class OrElse[-A, +B](f1: PartialFunction[A, B], f2: PartialFunction[A, B]) extends PartialFunction[A, B] {
    def isDefinedAt(x: A) = f1.isDefinedAt(x) || f2.isDefinedAt(x)

    def apply(x: A): B = f1.applyOrElse(x, f2)

    override def applyOrElse[A1 <: A, B1 >: B](x: A1, default: A1 => B1): B1 = {
      val z = f1.applyOrElse(x, checkFallback[B])
      if (!fallbackOccurred(z)) z else f2.applyOrElse(x, default)
    }

    override def orElse[A1 <: A, B1 >: B](that: PartialFunction[A1, B1]) =
      new OrElse[A1, B1](f1, f2 orElse that)

    override def andThen[C](k: B => C) =
      new OrElse[A, C](f1 andThen k, f2 andThen k)
  }

  /** `PartialFunction#andThen` 方法生成的复合函数 */
  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)
    }
  }

  /**
   * 为了高效地实现类似于 `if(pf isDefinedAt x) f1(pf(x)) else f2(x)` 这样的模式,
   * 使用以下技巧:
   *
   * 为了避免模式匹配和守卫条件的二次评估,此处使用了 `applyOrElse` 方法而不是 `isDefinedAt`/`apply` 组合。
   *
   * 在调用 `applyOrElse` 后,我们需要函数返回的结果以及函数的参数是否包含在其定义域中的信息。
   * 我们唯一可以调整的地方是 `applyOrElse` 方法的继续参数(`default`)。
   * 显而易见的方法是从 `default` 函数抛出异常,并在调用 `applyOrElse` 后捕获它,但我认为这种方式效率较低。
   *
   * 我只知道一种方法可以高效地完成这个任务:`default` 函数应该返回一个唯一的标记对象,该对象在任何其他(常规/偏)函数中都不会返回。
   * 这样,在调用 `applyOrElse` 后,您只需要进行一次引用比较即可区分是否 `pf isDefined x`。
   *
   * 这与特化正确交互,因为 `applyOrElse` 的返回类型(参数化上界)永远不能特化。
   *
   * 这里的 `fallback_pf` 既是唯一的标记对象,也是返回它的特殊回退函数。
   */
  private[this] val fallback_pf: PartialFunction[Any, Any] = { case _ => fallback_pf }
  private def checkFallback[B] = fallback_pf.asInstanceOf[PartialFunction[Any, B]]
  private def fallbackOccurred[B](x: B) = (fallback_pf eq x.asInstanceOf[AnyRef])

  /**
   * 将偏函数转换为接受参数类型为 A、返回值类型为 Option[B] 的普通函数。
   */
  private class Lifted[-A, +B](val pf: PartialFunction[A, B])
    extends scala.runtime.AbstractFunction1[A, Option[B]] {

    def apply(x: A): Option[B] = {
      val z = pf.applyOrElse(x, checkFallback[B])
      if (!fallbackOccurred(z)) Some(z) else None
    }
  }

  /**
   * 将函数转换为偏函数。
   * @since 2.10
   */
  def apply[A, B](f: A => B): PartialFunction[A, B] = { case x => f(x) }

  private[this] val constFalse: Any => Boolean = { _ => false }

  /**
   * 空定义域的偏函数。
   * 任何尝试调用空偏函数的操作都会导致抛出 [[scala.MatchError]] 异常。
   * @since 2.10
   */
  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
  }

  /**
   * 空定义域的偏函数。
   */
  def empty[A, B]: PartialFunction[A, B] = empty_pf

  /**
   * 创建基于值和偏函数的布尔测试。
   * 它类似于带有暗示的 `match` 语句,其后跟随所提供的情况之后的 `case _ => false`。
   *
   * @param x  要测试的值
   * @param pf 偏函数
   * @return 如果 `x` 在 `pf` 的定义域内且 `pf(x) == true`,则返回 true;否则返回 false。
   */
  def cond[T](x: T)(pf: PartialFunction[T, Boolean]): Boolean = pf.applyOrElse(x, constFalse)

  /**
   * 将偏函数 `pf` 转换为接受参数类型为 T、返回值类型为 Option[U] 的函数 `f`,
   * 并将其应用于值 `x`。实际上,它是一个 `'''match'''` 语句,
   * 其中所有的 case 结果都被包装在 `Some(_)` 中,并在末尾添加了 `'''case''' _ => None`。
   *
   * @param x  要测试的值
   * @param pf 偏函数
   * @return 如果 `pf isDefinedAt x`,则返回 `Some(pf(x))`;否则返回 None。
   */
  def condOpt[T, U](x: T)(pf: PartialFunction[T, U]): Option[U] = pf.lift(x)
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BigDataMLApplication

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值