【问题标题】:Using Either to process failures in Scala code使用 Either 处理 Scala 代码中的故障
【发布时间】:2010-11-14 16:00:06
【问题描述】:

Option monad 是在 Scala 中处理“有或无”事物的一种极好的表达方式。但是,如果在“什么都没有”发生时需要记录一条消息怎么办?根据 Scala API 文档,

Either 类型通常用作 替代 scala.Option where Left 代表失败(按照惯例)和 权利类似于 Some。

但是,我没有运气找到使用 Either 的最佳实践或涉及 Either 处理故障的真实世界示例。最后,我为自己的项目提出了以下代码:

    def logs: Array[String] = {
        def props: Option[Map[String, Any]] = configAdmin.map{ ca =>
            val config = ca.getConfiguration(PID, null)
            config.properties getOrElse immutable.Map.empty
        }
        def checkType(any: Any): Option[Array[String]] = any match {
            case a: Array[String] => Some(a)
            case _ => None
        }
        def lookup: Either[(Symbol, String), Array[String]] =
            for {val properties <- props.toRight('warning -> "ConfigurationAdmin service not bound").right
                 val logsParam <- properties.get("logs").toRight('debug -> "'logs' not defined in the configuration").right
                 val array <- checkType(logsParam).toRight('warning -> "unknown type of 'logs' confguration parameter").right}
            yield array

        lookup.fold(failure => { failure match {
            case ('warning, msg) => log(LogService.WARNING, msg)
            case ('debug, msg) =>   log(LogService.DEBUG, msg)
            case _ =>
        }; new Array[String](0) }, success => success)
    }

(请注意这是一个真实项目的sn-p,所以它不会自行编译)

我很高兴知道您如何在代码中使用 Either 和/或重构上述代码的更好想法。

【问题讨论】:

  • 我在奥德斯基的书中也找不到任何提及。
  • 是的,我有“Scala 编程”,但在其中找不到任何提及 Either。我所知道的最好的类比是 Liftweb 中的 Box,它也用于承载故障——它类似于 Option,但具有额外的功能。
  • 有什么比Option[Either[Foo, Bar]]更好的替代品吗?

标签: scala functional-programming either


【解决方案1】:

Either 用于返回可能的两个有意义的结果之一,不像 Option 用于返回单个有意义的结果或什么都不返回。

下面给出了一个易于理解的示例(不久前在 Scala 邮件列表中流传):

def throwableToLeft[T](block: => T): Either[java.lang.Throwable, T] =
  try {
    Right(block)
  } catch {
    case ex => Left(ex)
  }

顾名思义,如果“block”执行成功,会返回“Right()”。否则,如果抛出 Throwable,它将返回“Left()”。使用模式匹配处理结果:

var s = "hello"
throwableToLeft { s.toUpperCase } match {
  case Right(s) => println(s)
  case Left(e) => e.printStackTrace
}
// prints "HELLO"

s = null
throwableToLeft { s.toUpperCase } match {
  case Right(s) => println(s)
  case Left(e) => e.printStackTrace
}
// prints NullPointerException stack trace

希望对您有所帮助。

【讨论】:

  • 奇特的...为什么不直接抛出异常呢?
  • 到处都有异常处理代码是丑陋且难以管理的。使用 throwableToLeft 将异常处理转换为模式匹配,恕我直言,更易于阅读和维护。
  • 例如,您可能有多个参与者同时进行不同的计算,其中一些实际返回结果,而另一些则抛出异常。如果你只是抛出异常,其中一些演员可能还没有开始工作,你会失去任何尚未完成的演员的结果,等等。使用这种方法,所有演员都会返回一个值(一些Left,一些Right),它最终变得更容易处理。
  • @skaffman 另外,你不能“发送异常”,比如从一个演员到另一个演员。这允许发送异常。
  • 在 Scala 2.10 中有 scala.util.Try 类,它本质上是 Either 的一个特例,用于异常处理,所以如果你正在做类似上面例子的事情,那么这是需要考虑的事情。
【解决方案2】:

Scalaz 库有类似的命名验证。它比 Either 更惯用,用作“获得有效结果或失败”。

验证还允许累积错误。

编辑:“alike” Either 是完全错误的,因为 Validation 是一个应用函子,而名为 \/(发音为“disjonction”或“either”)的 scalaz Either 是一个 monad。 验证可以累积错误的事实是因为这种性质。另一方面, / 具有“提前停止”的性质,在遇到的第一个 -\/ 处停止(读作“左”或“错误”)。这里有一个完美的解释:http://typelevel.org/blog/2014/02/21/error-handling.html

见:http://scalaz.googlecode.com/svn/continuous/latest/browse.sxr/scalaz/example/ExampleValidation.scala.html

根据评论的要求,复制/粘贴上述链接(删除了一些行):

// Extracting success or failure values
val s: Validation[String, Int] = 1.success
val f: Validation[String, Int] = "error".fail

// It is recommended to use fold rather than pattern matching:
val result: String = s.fold(e => "got error: " + e, s => "got success: " + s.toString)

s match {
  case Success(a) => "success"
  case Failure(e) => "fail"
}

// Validation is a Monad, and can be used in for comprehensions.
val k1 = for {
  i <- s
  j <- s
} yield i + j
k1.toOption assert_≟ Some(2)

// The first failing sub-computation fails the entire computation.
val k2 = for {
  i <- f
  j <- f
} yield i + j
k2.fail.toOption assert_≟ Some("error")

// Validation is also an Applicative Functor, if the type of the error side of the validation is a Semigroup.
// A number of computations are tried. If the all success, a function can combine them into a Success. If any
// of them fails, the individual errors are accumulated.

// Use the NonEmptyList semigroup to accumulate errors using the Validation Applicative Functor.
val k4 = (fNel <**> fNel){ _ + _ }
k4.fail.toOption assert_≟ some(nel1("error", "error"))

【讨论】:

  • 很高兴在答案中看到一个示例。适用于问题中提出的问题类型。
  • flatMap,因此对于 Validation 的理解在 Scalaz 7.1 中已被弃用并在 Scalaz 7.1 中被删除。答案中的示例不再有效。见deprecation discussion
【解决方案3】:

您发布的 sn-p 似乎很做作。您在以下情况下使用 Either:

  1. 仅仅知道数据不可用是不够的。
  2. 您需要返回两种不同类型之一。

确实,将异常转化为 Left 是一个常见的用例。与 try/catch 相比,它具有将代码保持在一起的优势,如果异常是预期结果,这是有道理的。处理 Either 最常见的方式是模式匹配:

result match {
  case Right(res) => ...
  case Left(res) => ...
}

另一种处理Either 的有趣方式是当它出现在集合中时。在对集合进行映射时,抛出异常可能不可行,并且您可能希望返回“不可能”以外的一些信息。使用 Either 可以让您在不给算法负担过重的情况下做到这一点:

val list = (
  library 
  \\ "books" 
  map (book => 
    if (book \ "author" isEmpty) 
      Left(book) 
    else 
      Right((book \ "author" toList) map (_ text))
  )
)

这里我们得到了图书馆中所有作者的列表,加上没有作者的书籍列表。所以我们可以相应地进一步处理它:

val authorCount = (
  (Map[String,Int]() /: (list filter (_ isRight) map (_.right.get))) 
   ((map, author) => map + (author -> (map.getOrElse(author, 0) + 1)))
  toList
)
val problemBooks = list flatMap (_.left.toSeq) // thanks to Azarov for this variation

所以,基本的 Either 用法就是这样。这不是一个特别有用的类,但如果它是你以前见过的。另一方面,它也不是没用的。

【讨论】:

  • list flatMap {_.left.toSeq} 似乎返回了同样的问题书籍,对吧?
  • 是的,会的。我知道它有一个 flatMap 技巧,但是我在编写示例时找不到它。
【解决方案4】:

Cats 有一个很好的方法来从抛出异常的代码中创建一个 Either:

val either: Either[NumberFormatException, Int] =
  Either.catchOnly[NumberFormatException]("abc".toInt)
// either: Either[NumberFormatException,Int] = Left(java.lang.NumberFormatException: For input string: "abc")

https://typelevel.org/cats/datatypes/either.html#working-with-exception-y-code

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-01-23
    • 1970-01-01
    • 2015-12-28
    • 2015-01-26
    • 1970-01-01
    • 1970-01-01
    • 2012-04-29
    相关资源
    最近更新 更多