【问题标题】:Already existing functional way for a retry-until in Scala?在 Scala 中是否已经存在用于重试的功能方式?
【发布时间】:2015-02-13 18:41:30
【问题描述】:

是否有一种函数式/Scala 方法可以重复调用一个函数直到它成功,同时对失败的尝试做出反应?

让我用一个例子来说明。假设我想从标准输入中读取一个整数,如果用户实际上没有输入整数,则重试。

给定这个函数:

def read_int(): Either[String, Int] = {
  val str = scala.io.StdIn.readLine()
  try {
    Right(str.trim().toInt)
  } catch {
    case _: java.lang.NumberFormatException => Left(str)
  }
}

还有这个匿名函数:

val ask_for_int = () => {
  println("Please enter an Int:")
  read_int()
}

val handle_not_int = (s: String) => {
  println("That was not an Int! You typed: " + s)
}

我会这样使用它们:

val num = retry_until_right(ask_for_int)(handle_not_int)
println(s"Thanks! You entered: $num")

我的问题是:

  • Scala 中是否已经存在 retry_until_right 之类的内容?
  • 能否用现有设施解决? (流、迭代器、Monad 等)
  • 是否有任何 FP 库(scalaz?)提供类似的功能?
  • 我可以做得更好/更惯用吗? (*)

谢谢!

*) 除了snake_case。我真的喜欢它。

【问题讨论】:

  • 我不认为我的问题与那个问题重复。那个是特定于异常失败的,并且重试的数量是有编号的。我的问题更抽象。另一个问题可能是我的一个特殊情况。这个问题也是“实现它的最佳方法是什么?”,我的问题是“我必须实现它吗?” (或者它已经存在?)

标签: scala functional-programming scalaz


【解决方案1】:

我认为Try monad 与Iterator.continually 方法一起适用于这个一般问题。当然,如果您愿意,可以采用这个答案来使用Either

def retry[T](op: => Try[T])(onWrong: Throwable => Any) = 
    Iterator.continually(op).flatMap { 
        case Success(t) => Some(t)
        case Failure(f) => onWrong(f); None 
    }.toSeq.head

那么你可以这样做:

retry { Try(scala.io.StdIn.readLine.toInt) }{ _ => println("failed!") }

或者您甚至可以隐藏实现的Try 部分,并给onWrong 一个默认值,并使其成为第二个参数而不是柯里化函数:

def retry[T](op: => T, onWrong: Throwable => Any = _ => ()) = 
    Iterator.continually(Try(op)).flatMap { 
        case Success(t) => Some(t)
        case Failure(f) => onWrong(f); None 
    }.toSeq.head

那么你可以简单地:

retry { scala.io.StdIn.readLine.toInt } { _ => println("failed") }

或者

retry { scala.io.StdIn.readLine.toInt }

【讨论】:

  • 我看到您的方法与我的方法相似(连续/flatMap/toSeq/head)。很高兴看到我走在正确的轨道上。所以看起来这不是内置的。也许 scalaz、shapeless 或其中一些有这样的东西......
【解决方案2】:

这是使用 scalaz.concurrent.Task 的替代解决方案:

import scalaz.concurrent.Task

def readInt: Task[Int] = {
  Task.delay(scala.io.StdIn.readLine().trim().toInt).handleWith {
    case e: java.lang.NumberFormatException =>
      Task.delay(println("Failure!")) flatMap (_ => readInt)
  }
}

还有一个重试包装器(不太灵活):

def retry[A](f: Task[A])(onError: PartialFunction[Throwable, Task[_]]): Task[A] =
  f handleWith (onError andThen (_.flatMap(_ => retry(f)(onError))))

val rawReadInt: Task[Int] = Task.delay(scala.io.StdIn.readLine().trim().toInt)

val readInt: Task[Int] = retry(rawReadInt) {
  case e: java.lang.NumberFormatException => Task.delay(println("Failure!"))
}

说明

scalaz.concurrent.Task[A] 是一个一元结构,最终返回一个A。它使用蹦床(通常)避免堆栈溢出。它还处理异常,并且可以重新抛出异常,或者通过\/ 表示异常(scalaz 的右偏Either)。

handleWith 允许为Throwable 编写处理程序,该处理程序由Task 引发。这个处理程序的结果是一个新的Task 之后运行。在这种情况下,我们将只打印一条错误消息,并使用flatMap 再次调用原始的Task。因为Task 是一个蹦床结构,所以这应该是安全的。

readInt.run试试这个 - 这将在当前线程上运行任务,并最终返回传入的 Int 值。

【讨论】:

  • 对不起...这里是新手...在这种情况下什么是“蹦床”?
  • 蹦床是一种去除堆栈溢出的策略;它将分层计算表示为数据结构而不是一组堆栈帧。 “蹦床”是一个执行框架,它可以逐步处理这个数据结构,直到评估最终结果。来自 scalaz.concurrent.Future 的 scaladoc:“Future 是一种蹦床计算,产生一个可能包括异步步骤的 A。像 Trampoline 一样,涉及 map 和 flatMap 的任意单子表达式保证使用常量堆栈空间。”另请参阅:“Scala 中的函数式编程”,Ch。 13.
  • Rúnar Óli Bjarnason 的论文“Stackless Scala With Free Monads”也许是一个更好的参考:blog.higher-order.com/assets/trampolines.pdf
【解决方案3】:

这是我的第一个尾递归实现:

@scala.annotation.tailrec
def retry_until_right[WRONG, RIGHT](generator: () => Either[WRONG, RIGHT])(on_wrong: WRONG => Any): RIGHT = {
  generator() match {
    case Right(right) =>
      right
    case Left(wrong) =>
      on_wrong(wrong)
      retry_until_right(generator)(on_wrong)
  }
}

但是想重用现有的库,然后我切换到使用迭代器的这种方法:

def retry_until_right[WRONG, RIGHT](generator: => Either[WRONG, RIGHT])(on_wrong: WRONG => Any): RIGHT =
  Iterator.continually(generator).flatMap {
    case Left(value) =>
      on_wrong(value)
      None
    case Right(value) =>
      Some(value)
  }.toSeq.head

可以这样使用:

val num = retry_until_right(ask_for_int()) { str =>
  println("Ivalid input: " + str)
}
println("Thanks! You entered: " + num)

但是,可以说将Iterator 隐藏在实现内部的普通视图中可能是不灵活的。如果开发人员想对其进行进一步的操作怎么办? (映射等)

相反,对“错误”值做出反应并最终选择第一个“正确”值的操作可以抽象为特定于 Either[L,R] 类型迭代器的“扩展”类,如下所示:

implicit class EitherIteratorExtensions[L, R](it: Iterator[Either[L, R]]) {
  def onLeft(callback: L => Any) =
    it.map {
      case left @ Left(value) =>
        callback(value)
        left
      case right => right
    }

  // take only Right elements
  def takeRight: Iterator[R] = 
    it.flatMap {
      case Left(_) =>
        None
      case Right(value) => Some(value)
    }

  // iterate and fetch the first Right element
  def firstRight: R = {
    takeRight.toSeq.head
  }
}

现在我们可以轻松地以简洁的代码使用所需的方法,同时保持对Iterator 的控制,如下所示:

val num = Iterator.continually(ask_for_int()).onLeft(handle_not_int).firstRight

println("Thanks! You entered: " + num)

虽然我对这种方法很满意,但我仍然想知道这是否不是现有库的一部分已经...

【讨论】:

  • toSeq 不会产生无限集合吗?也许你想要一个 BufferedIterator 来获得头部?
  • @wheaties - 我认为不是。 toList 似乎强制收集,而不是 toSeq。但我可能弄错了。至少我试过了,它有效。
  • 还要注意,它不能成功生成无限集合,因为它依赖于 ask_for_int() 的输入。它必须等待它。如果 ask_for_int() 从未返回 Int,则会导致无限循环。但无论如何,这就是想法。
【解决方案4】:
def retry[L, R](f: => Either[L, R])(handler: L => Any): R = {
    val e = f
    e.fold(l => { handler(l); retry(f)(handler) }, identity)
}

【讨论】:

  • 折叠的使用非常有趣,尤其是identity。对我来说是新的。
  • 与@SebastianN 函数折叠时使用身份函数是一件非常自然的事情。 .与折叠和时 0 是自然种子和折叠乘积时 1 相同,身份是折叠将建立累积函数的自然种子。如果集合为空,您将收到一个仅返回其参数的函数。如果不是,您会收到一个迭代函数,它会转换参数(身份是无害的初始或最终阶段)。对集合、Option、Either 等很有用。
【解决方案5】:

可以在此处找到重试 monad 的实现: https://github.com/hipjim/scala-retry 它有多种重试策略。

// define the retry strategy
implicit val retryStrategy =
    RetryStrategy.fixedBackOff(retryDuration = 1.seconds, maxAttempts = 2)

// pattern match the result
val r = Retry(1 / 1) match {
    case Success(x) => x
    case Failure(t) => log("I got 99 problems but you won't be one", t)
}

【讨论】:

    猜你喜欢
    • 2011-01-31
    • 2014-04-20
    • 1970-01-01
    • 1970-01-01
    • 2017-02-24
    • 2022-08-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多