已经存在重试的功能方式 - 直到Scala?

Seb*_* N. 10 functional-programming scala scalaz

是否有函数/ 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)
  }
}
Run Code Online (Sandbox Code Playgroud)

而这个匿名函数:

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)
}
Run Code Online (Sandbox Code Playgroud)

我会像这样使用它们:

val num = retry_until_right(ask_for_int)(handle_not_int)
println(s"Thanks! You entered: $num")
Run Code Online (Sandbox Code Playgroud)

我的问题是:

  • retry_until_rightScala中已存在类似的东西吗?
  • 可以用现有设施解决吗?(流,迭代器,Monad等)
  • 任何FP库(scalaz?)都提供这样的东西吗?
  • 我可以做更好/更惯用的事情吗?(*)

谢谢!

*)除了snake_case.我真的很喜欢

Ben*_*ich 6

我认为Trymonad与该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
Run Code Online (Sandbox Code Playgroud)

然后你可以这样做:

retry { Try(scala.io.StdIn.readLine.toInt) }{ _ => println("failed!") }
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

那么你可以简单地说:

retry { scala.io.StdIn.readLine.toInt } { _ => println("failed") }
Run Code Online (Sandbox Code Playgroud)

要么

retry { scala.io.StdIn.readLine.toInt }
Run Code Online (Sandbox Code Playgroud)


Gar*_*ady 5

这是使用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)
  }
}
Run Code Online (Sandbox Code Playgroud)

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

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!"))
}
Run Code Online (Sandbox Code Playgroud)

说明

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

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

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

  • Trampolining是一种消除堆栈溢出的策略; 它将分层计算表示为数据结构而不是一组堆栈帧.然后,"trampoline"是一个执行框架,可以逐步处理该数据结构,直到评估最终结果.来自scalazoc for scalaz.concurrent.Future:"Future是一个蹦床计算产生一个可能包含异步步骤的A.就像Trampoline一样,涉及map和flatMap的任意monadic表达式都保证使用不变的堆栈空间." 另请参见:"Scala中的函数编程",Ch.13. (2认同)

Seb*_* N. 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)
  }
}
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

可以像这样使用:

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

然而,有人可能会说,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
  }
}
Run Code Online (Sandbox Code Playgroud)

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

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

println("Thanks! You entered: " + num)
Run Code Online (Sandbox Code Playgroud)

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