使用 Either 处理 Scala 代码中的故障

发布于 2024-07-29 21:04:56 字数 1562 浏览 13 评论 0原文

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

Either 类型通常用作 scala.Option where Left 的替代方案 代表失败(按照惯例)并且 Right 类似于 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)
    }

请注意,这是来自真实项目的片段,因此它不会自行编译)

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

Option monad is a great expressive way to deal with something-or-nothing things in Scala. But what if one needs to log a message when "nothing" occurs? According to the Scala API documentation,

The Either type is often used as an
alternative to scala.Option where Left
represents failure (by convention) and
Right is akin to Some.

However, I had no luck to find best practices using Either or good real-world examples involving Either for processing failures. Finally I've come up with the following code for my own project:

    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)
    }

(Please note this is a snippet from a real project, so it will not compile on its own)

I'd be grateful to know how you are using Either in your code and/or better ideas on refactoring the above code.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

鹤仙姿 2024-08-05 21:04:56

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

希望有帮助。

Either is used to return one of possible two meaningful results, unlike Option which is used to return a single meaningful result or nothing.

An easy to understand example is given below (circulated on the Scala mailing list a while back):

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

As the function name implies, if the execution of "block" is successful, it will return "Right(<result>)". Otherwise, if a Throwable is thrown, it will return "Left(<throwable>)". Use pattern matching to process the result:

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

Hope that helps.

﹎☆浅夏丿初晴 2024-08-05 21:04:56

Scalaz 库有类似的东西,名为 Validation。 它比“Either”更惯用,用作“获得有效结果或失败”。

验证还允许累积错误。

编辑:“alike” Either 是完全错误的,因为 Validation 是一个应用函子,而 scalaz Either,名为 \/ (发音为“disjonction”或“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"))

Scalaz library has something alike Either named Validation. It is more idiomatic than Either for use as "get either a valid result or a failure".

Validation also allows to accumulate errors.

Edit: "alike" Either is complettly false, because Validation is an applicative functor, and scalaz Either, named \/ (pronounced "disjonction" or "either"), is a monad.
The fact that Validation can accumalate errors is because of that nature. On the other hand, / has a "stop early" nature, stopping at the first -\/ (read it "left", or "error") it encounters. There is a perfect explanation here: http://typelevel.org/blog/2014/02/21/error-handling.html

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

As requested by the comment, copy/paste of the above link (some lines removed):

// 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"))
复古式 2024-08-05 21:04:56

您发布的片段看起来非常做作。 您可以在以下情况下使用 Either:

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

将异常转变为左确实是一个常见的用例。 与 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 用法就是这样的。 这不是一个特别有用的类,但如果是的话,您以前应该已经见过它了。 另一方面,它也并非毫无用处。

The snippet you posted seems very contrived. You use Either in a situation where:

  1. It's not enough to just know the data isn't available.
  2. You need to return one of two distinct types.

Turning an exception into a Left is, indeed, a common use case. Over try/catch, it has the advantage of keeping the code together, which makes sense if the exception is an expected result. The most common way of handling Either is pattern matching:

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

Another interesting way of handling Either is when it appears in a collection. When doing a map over a collection, throwing an exception might not be viable, and you may want to return some information other than "not possible". Using an Either enables you to do that without overburdening the algorithm:

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

Here we get a list of all authors in the library, plus a list of books without an author. So we can then further process it accordingly:

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

So, basic Either usage goes like that. It's not a particularly useful class, but if it were you'd have seen it before. On the other hand, it's not useless either.

巴黎夜雨 2024-08-05 21:04:56

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

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

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

Cats has a nice way to create an Either from exception-throwing code:

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

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

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文