【问题标题】:How to convert functions raising exceptions to functions returning Either?如何将引发异常的函数转换为返回 Either 的函数?
【发布时间】:2014-05-13 14:14:40
【问题描述】:

假设我有一些引发异常的函数。我将它们包装起来返回Either[Throwable, <function return type>]。 (假设我需要Either 而不是Try)。

def fooWrapper(arg1: FooArg1, arg2: FooArg2) =
  try Right(foo(arg1, arg2)) catch { case NonFatal(e) => Left(e) }

def barWrapper(arg1: BarArg1, arg2: BarArg2, a3: BarArg3) =
  try Right(bar(arg1, arg2, artg3)) catch { case NonFatal(e) => Left(e) }

...

现在我想编写一个 generic 包装器来摆脱 bolierpllate 代码。你有什么建议?

【问题讨论】:

    标签: scala exception-handling either


    【解决方案1】:

    任何时候你想在 arity 方面做一些通用的东西,Shapeless 很可能有你需要的东西。在这种情况下,您可以编写以下内容:

    import scala.util.control.NonFatal
    import shapeless._, ops.function._
    
    def safeify[F, A <: HList, R, O](f: F)(implicit
      ftp: FnToProduct.Aux[F, A => R],
      ffp: FnFromProduct[A => Either[Throwable, R]]
    ) = ffp((a: A) =>
      try Right(ftp(f)(a)) catch {
        case NonFatal(ex) => Left(ex)
      }
    )
    

    现在假设我们有一个不安全的方法,如下所示:

    def bad(s: String, i: Int) = s.toInt / i
    

    我们可以包装它:

    scala> val better = safeify(bad _)
    better: (String, Int) => Either[Throwable,Int] = <function2>
    

    现在我们不必担心异常:

    scala> better("1", 0)
    res0: Either[Throwable,Int] = Left(ArithmeticException: / by zero)
    
    scala> better("a", 1)
    res1: Either[Throwable,Int] = Left(NumberFormatException: For input string: "a")
    

    这适用于任何旧的FunctionN

    【讨论】:

    • 谢谢。它看起来很有趣,但相当先进。我会在一边玩它,但不会将它推送到生产仓库中。我会改用wheaties 解决方案。
    【解决方案2】:

    我会写一些像这样的形式:

     def wrap[Value](f: => Value): Either[Value, Exception] = try{
       Right(f).right
     }
     catch{
       case ex: Exception => Left(ex).right
     }
    
     def foo(arg1: FooArg1, arg2: FooArg2) = wrap{
       //anything I'd have written before in foo
     }
    

    但这并不构成。 Try 好多了。

    更新:如果您只想处理正确的投影,那么只需返回正确的投影。现在它组成了。

    【讨论】:

    • “这不构成”是什么意思?
    • @Michael 在Either 上做mapflatMap 没有真正的方法。 Try 的伟大之处在于它可以组合,以便您可以处理多个返回 Try 的表达式。
    • 我认为他的意思是Try 可以用于理解,因此您可以编写一系列操作,其中任何一个都可以抛出异常,如果是这样,则停止处理并且引发的异常是返回。
    • @wheaties 你是对的 Either 没有 mapflatMap 但它的预测有。由于 Right 投影 do 组成,我可以将它们用于理解等。
    • @wheaties 我将处理它的 right 投影。例如,for {r1 &lt;- wrap(foo(...)).right; r2 &lt;- wrap(bar(...)).right} yield e2
    【解决方案3】:

    如果您不想修改初始函数定义:

    您是否需要一个适用于任意数量参数的包装器,或者您是否可以为每个可能数量的参数使用通用包装器?在后一种情况下,您可以:

    def eitherify[A, B, C](f: Function2[A, B, C])(a: A, b: B) = {
        try Right(f(a, b)) catch { case NonFatal(e) => Left(e) }
    }
    def eitherify[A, B, C, D](f: Function3[A, B, C, D])(a: A, b: B, c: C) = {
        try Right(f(a, b, c)) catch { case NonFatal(e) => Left(e) }
    }
    

    它允许你做eitherify(foo)eitherify(bar)等等。

    【讨论】:

    • 谢谢。对于具有 any 个参数的所有函数,我只想要 一个 包装器。
    • 正如您在函数文档 (scala-lang.org/api/2.11.0/index.html#scala.Function$) 上所见,必须为每个可能的参数数量单独定义一些包装器,例如 tupled、uncurried 等。所以我不确定这可以在没有任何额外的库和/或更重的机器的情况下完成
    【解决方案4】:

    由于您显然还没有足够的选择,另一种方法是使用ScalaUtils尝试方法。 ScalaUtils 是一个非常小的、专注的库,您可能不介意将其添加为依赖项。尝试方法(位于 org.scalautils 包对象中)将为您提供一个 Or 类型,您可以在该类型上调用 toEither。我首先建议使用 Try 执行此操作,这样您就不需要添加任何依赖项,但它似乎没有 toEither 方法,这让我感到惊讶。下面是 Or 的样子:

    scala> import org.scalautils._
    import org.scalautils._
    
    scala> def foo(i: Int, s: String): String = { require(i >= 0); s * i }
    foo: (i: Int, s: String)String
    
    scala> def bar(b: Boolean, i: Int, s: String): Int = { require(i >= 0); if (b) s.length else i }
    bar: (b: Boolean, i: Int, s: String)Int
    
    scala> def foo2(i: Int, s: String) = attempt(foo(i, s)).toEither
    foo2: (i: Int, s: String)Either[Throwable,String]
    
    scala> def bar2(b: Boolean, i: Int, s: String) = attempt(bar(b, i, s)).toEither
    bar2: (b: Boolean, i: Int, s: String)Either[Throwable,Int]
    
    scala> foo2(2, "ho")
    res10: Either[Throwable,String] = Right(hoho)
    
    scala> foo2(-2, "ho")
    res11: Either[Throwable,String] = Left(java.lang.IllegalArgumentException: requirement failed)
    
    scala> bar2(true, 3, "ho")
    res12: Either[Throwable,Int] = Right(2)
    
    scala> bar2(false, 3, "ho")
    res13: Either[Throwable,Int] = Right(3)
    
    scala> bar2(false, -3, "ho")
    res14: Either[Throwable,Int] = Left(java.lang.IllegalArgumentException: requirement failed)
    

    抱歉,我最初错过了您想要一种方法。我可能会像 noziar 建议的那样超载:

    scala> :paste
    // Entering paste mode (ctrl-D to finish)
    
    def safely[A, B, C](f: (A, B) => C): (A, B) => Either[Throwable, C] = (a: A, b: B) => attempt(f(a, b)).toEither
    def safely[A, B, C, D](f: (A, B, C) => D): (A, B, C) => Either[Throwable, D] = (a: A, b: B, c: C) => attempt(f(a, b, c)).toEither
    
    
    // Exiting paste mode, now interpreting.
    
    safely: [A, B, C](f: (A, B) => C)(A, B) => Either[Throwable,C] <and> [A, B, C, D](f: (A, B, C) => D)(A, B, C) => Either[Throwable,D]
    safely: [A, B, C](f: (A, B) => C)(A, B) => Either[Throwable,C] <and> [A, B, C, D](f: (A, B, C) => D)(A, B, C) => Either[Throwable,D]
    
    scala> val foo3 = safely { foo _ }
    foo3: (Int, String) => Either[Throwable,String] = <function2>
    
    scala> val bar3 = safely { bar _ }    
    bar3: (Boolean, Int, String) => Either[Throwable,Int] = <function3>
    
    scala> foo3(2, "ho")
    res5: Either[Throwable,String] = Right(hoho)
    
    scala> foo3(-2, "ho")
    res6: Either[Throwable,String] = Left(java.lang.IllegalArgumentException: requirement failed)
    
    scala> bar3(true, 3, "ho")
    res7: Either[Throwable,Int] = Right(2)
    
    scala> bar3(false, 3, "ho")
    res8: Either[Throwable,Int] = Right(3)
    
    scala> bar3(false, -3, "ho")
    res9: Either[Throwable,Int] = Left(java.lang.IllegalArgumentException: requirement failed)
    

    如果你想避免重载(和无形),那么另一种选择是magnet pattern。我相信这会让你找到一种安全的方法,但我认为重载会更简单。

    【讨论】:

      【解决方案5】:

      Scalaz 有一个偏右的Either,称为\/,它可能满足您的需求,正如您所描述的那样。您可以在 \/ 对象上使用 fromTryCatch 来删除提到的样板。由于\/ 是右偏的,它可以用于理解,偏右。

      示例用法可能是:

      for {
        r1 <- \/.fromTryCatch(something1)
        r2 <- \/.fromTryCatch(something2)
      } yield something3(r1, r2)
      

      上面的结果类型是Throwable \/ A

      【讨论】:

      • 感谢您的建议。我现在只需要Either
      • \/Either 同构,并且具有更好的 API
      • 我同意\/ 看起来更好,但我只是在系统中添加了一个小组件,它使用Either 而不是\/,并且根本不依赖scalaz。我不确定我是否只想为我的小组件添加 scalaz 依赖项。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多