如何优雅地处理错误?

简述

前面说过, 抛出异常会产生副作用; 但是如果不能抛出异常, 该怎么处理呢?

在函数式编程的解决方案里, 以值的方式返回错误更安全, 符合引用透明, 并且可以通过高阶函数保存异常的优点 – 统一处理错误逻辑

异常的优劣

我们来看一个简单的例子

def failingFn (i: Int): Int = {
  var y: Int = throw new Exception("fail!")
  try {
    var x = 42 + 5
    x + y
  }
  catch {
    case e: Exception => 43
  }
}

在 repl 下调用就会出现错误, 如预期抛出异常, 在上面这段代码里, y 不是引用透明的, 因为这个表达式不能被它引用的值替代, 如果我们把 y 替换成 throw new Exception(...)会产生不同的结果

def failingFn (i: Int): Int = {
  try {
    var x = 42 + 5
    x + ((throw new Exception("fail!")): Int)
  }
  catch {
    case e: Exception => 43
  }
}

引用透明是不依赖上下文(Context)的, 并且不需要全局推导; 异常存在两个严重的问题:

  • 异常破坏了引用透明并引入了上下文依赖
  • 异常不是类型安全的 failingFn Int => Int 可能会抛出运行时错误

如果有一套系统既能保证整合集中的错误处理逻辑又能刨除异常这些缺点, 岂不美哉?
这里使用了一个理念: 返回一个值来表示异常发生来替代异常; 我们引入一种新的泛型类型来描述这些 "可能存在定义的值", 并使用高阶函数来封装这种处理和传播异常的通用模式, 而不是像错误码那样; 错误处理策略是完全类型安全的, 并且能够进行类型检测;

异常的其他选择

这里有个函数计算平均值, 正常的函数 抛出异常;

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

稍微函数式的解法, 针对那些输入不知道怎么处理的情况, 强迫调用者告诉我们一个参数怎么处理

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

但是另外的问题来了, 它需要调用者知道如何处理未定义的情况, 限制它们返回一个 Double, 如果在复杂的运算里会成为一个很大的问题; 我们需要一种方式能推迟决定如何处理未定义的情况, 可以在最合适的时候处理;

Option 数据类型

解决方案是: 在返回值类型时明确表示该函数并不总是有答案; 可以理解为这是用于推迟调用者的错误处理策略; 我们引入一种新的数据类型 Option;

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

Option 有两种情况: 已经定义的情况对应的是 Some; 未被定义的情况应该是 None; 我们来改造下 mean 函数

def mean(xs: Seq[Double]): Option[Double] =
  if (xs.isEmpty) None
  else Some(xs.sum / xs.length)

返回值类型现在反映了一种可能性: 结果不一定总被定义; 现在 mean 是一个完全函数, 对每一个输入的值都有对应输出类型的值;

Either 数据类型

本文的核心概念是我们可以用普通的值类表示失败和异常, 将对错误处理和恢复的通用模式抽象出来; Option 不是用于这种目的的唯一数据类型, 它有些过于简单, 只给我们一个 None 表示没有可用的值; 但是有时候我们想知道更多信息, 比如想要一个字符串告诉我们实际错误是什么;

可以创建一个数据类型, 对我们想要的失败信息进行编码, 如果只需要知道是否失败, 可以用 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 代表成功, 接着改造 mean 函数:

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

看到这里 相信你应该对 swift 和 scala 等函数式编程语言的异常类型系统有了更新的了解; 明白语言作者为什么引入这样的设计 以及好处;^_^