CH04-异常

抛出异常会产生副作用,在函数式解决方案中,以值的方式返回错误更安全,符合引用透明,并且可以通过高阶函数保存异常的优点 - 统一错误处理逻辑。核心思想就是使用普票值来表现异常。

异常的优点与劣势

一个抛出异常的例子:

def failingFn(i: Int): Int = {
  val y:Int = throw new Exception("fail!") 	// 抛出异常
  try{
    val x = 42 + 5
    x + y
  } catch {
    case e:Exception => 43
  }
}

调用该函数会抛出预期的异常,为了证明异常会破坏引用透明,我们可以将y进行替换:

def failingFn2(i: Int) = {
  try{
    val x = 42 + 5
    x + (throw new Exception("fail!"): Int)	// 抛出异常的表达式可以声明为任何类型
  } catch {
    case e: Exception => 43
  }
}

运行结果会得到 43 而不是像替换前一样排除预期的异常,因此,异常并不是引用透明的。

引用透明的表达式不依赖上下文,可以本地推导,而那些非引用透明的表达式是依赖上下文的,并且需要全局推导。比如把42 + 5这个表达式嵌入到一个更大的表达式中,他不会受这个更大的表达式的影响,即对更大的表达式产生依赖,它永远等于 47。但是把throw new...嵌入到一个更大的表达式,比如嵌入到一个try/catch结构中,它的值取决于catch部分的处理,因此对更大的表达式产生了依赖。

异常的问题:

  • 异常破坏了引用透明并且引入了上下文依赖。
  • 异常不是类型安全的。函数failingInt => Int并不会告诉我们会发生什么样的异常。因为没有受检异常,直到运行时才会发现异常。

我们希望其他选择能够排除异常的这些缺点,但是又不想失去异常最主要的好处:整合集中的错误处理逻辑,而不是在整个代码库发布这个逻辑。

异常的其他选择

比如一个计算列表平均值的函数:

def mean(xs:Seq[Double]): Double = 
  if (xs.isEmpty) throw new ArithmeticException("mean of empty list!")
  else xs.sum / xs.length

这是一个典型的偏函数,他对一些输入没有做定义。如果一个函数对那些非隐式的输入类型做了一些假设,那它就是一个典型的偏函数。

除了选择抛出异常,还有其他的选择。

第一种是返回某个伪造的 Double 类型的值。比如为空时返回Double.NaN,或者其他报警值,或者null。但是以下理由使我们放弃这种方案:

  • 它允许错误无声的传播,如果忘了对这样的值进行检查也不会得到警告,会使后面的代码出错。
  • 使用显式的if来检查是否得到了正确的结果会导致大量的模板代码,特别是调用多个函数时。
  • 不适用与多态代码。比如一个查找最大值的泛型函数def max[A](xs:List[A]),当传入为空时无法发明一个 A 类型的值。也不能是null,因为null只对非基础类型有效,但是这里的 A 可能是IntDouble
  • 需要一个特定的策略或调用约定 - 告诉调用者如何合理的使用mean函数。这导致它不能传递给高阶函数,因为高阶函数对待所有参数都是一致的。

第二种是强迫调用者提供一个参数告诉我们该如何处理。比如:

def mean(xs:Seq[Double], onEmpty: Double): Double = 
  if (xs.isEmpty) onEmpty
  else xs.sum / xs.length

这使mean函数称为一个完全函数。调用者必须知道如何处理未定义的情况。

Option 数据类型

解决方案是在返回值类型明确表示该函数并不总是有结果。

sealed trait Option[+A]
case class Some[+A](get:A) extends Option[A]
case object None extends Option[A]

Option是一个最多只包含一个元素的List

基础 Option 函数

trait Option[+A] {
  def map[B](f: A => B): Option[B]					// 如果 Option 不为 None,对其应用 f
  def flatMap[B](f: A => Option[B]): Option[B] 		// 如果 Option 不为 None,对其应用 f,可能会失败
  def getOrElse[B >: A](default: B): B				// 默认值类型 B 必须是 A 的父类
  def orElse[B >: A](ob: => Option[B]): Option[B]	// 不对 ob 求值,除非需要
  def filter(f: A => Boolean): Option[A] 			// 如果值不满足,转换 Some 为 None
}

基础函数使用场景

???

Option 组合、提升、面向异常的 API 封装

可能会认为,一旦开始使用Option,整个库都会受影响,因为一些方法必须改变为接收或返回Option。但是,我们可以把一个普通函数**提升(lift)**为一个对Option操作的函数。

比如,map函数支持我们用一个类型为A => B的函数来操作一个Option。从另一个角度看,map可以把一个A => B的函数转换为Option[A] => Option[B]类型的函数:

def lift[A, B](f: A => B): Option[A] => Option[B] = _ map f

现在,可以把普通的abs函数转换为处理Option的函数:

val absOpt:Option[Double] => Option[Double] = lift(math.abs)

Try函数是一个通用目的的函数,用于将一个基于异常的 API 转换成一个面向Option的 API。

Either 数据类型

Option只能用来表示可能不存在的值,并不能表示异常条件下发生了什么错误。如果除了需要获取异常时可能的值,还需要知道异常时的错误信息,可以使用Either

sealed trait Either[+E, +A]
case class Left[+E](value: E) extends Either[E, Nothing]
case class Right[+A](value: A) extends Either[Nothing, A]

Either在两种情况都有值,它的值只能是两种情况中的一种,称他是两个类型的互斥并集。一般使用Left表示失败,而Right表示成功。

使用Either改写前面的mean函数:

def mean(xs:List[Double]): Either[String, Double] = 
  if (xs.isEmpty) Left("mean of empty list!")
  else Right(xs.sum / xs.length)

或者在Left中包含处理的异常以获取详细的调用栈信息:

def safeDiv(x:Int, y:Int):Either[Exception, Int] = 
  try Right(x / y)
  catch {case e:Exception => Left(e)}