在 Scala 中已经存在用于重试直到的功能方法?
Already existing functional way for a retry-until in Scala?
有没有一种 functional/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
之类的东西?
- 用现有的设施能解决吗? (流、迭代器、单子等)
- 是否有任何 FP 库(scalaz?)提供类似的东西?
- 我可以做得更好/更惯用吗? (*)
谢谢!
*) 除了 snake_case。我真的喜欢。
这是我的第一个尾递归实现:
@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
从普通视图中隐藏在实现中是不灵活的。如果开发者想对其进行进一步的操作怎么办? (映射等)
相反,对 "wrong" 值以及最后 select 第一个 "right" 值做出反应的操作可以抽象为特定于 "extension" class 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)
虽然我对这种方法很满意,但我仍然想知道这是否不是现有库已经的一部分...
def retry[L, R](f: => Either[L, R])(handler: L => Any): R = {
val e = f
e.fold(l => { handler(l); retry(f)(handler) }, identity)
}
我认为 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 }
这是使用 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]
是一个单子结构,最终 return 是一个 A
。它使用蹦床来(通常)避免堆栈溢出。它还处理异常,并且可以重新抛出异常,或者通过 \/
(scalaz 的右偏 Either
)表示异常。
handleWith
允许为由 Task
引发的 Throwable
编写处理程序。这个处理程序的结果是一个新的 Task
到 运行 之后。在这种情况下,我们将只打印一条错误消息,并使用 flatMap
再次调用原来的 Task
。由于 Task
是一个蹦床结构,因此 应该 是安全的。
尝试使用 readInt.run
- 这将 运行 当前线程上的任务,并最终 return 传入 Int 值。
可以在此处找到重试 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)
}
有没有一种 functional/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
之类的东西? - 用现有的设施能解决吗? (流、迭代器、单子等)
- 是否有任何 FP 库(scalaz?)提供类似的东西?
- 我可以做得更好/更惯用吗? (*)
谢谢!
*) 除了 snake_case。我真的喜欢。
这是我的第一个尾递归实现:
@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
从普通视图中隐藏在实现中是不灵活的。如果开发者想对其进行进一步的操作怎么办? (映射等)
相反,对 "wrong" 值以及最后 select 第一个 "right" 值做出反应的操作可以抽象为特定于 "extension" class 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)
虽然我对这种方法很满意,但我仍然想知道这是否不是现有库已经的一部分...
def retry[L, R](f: => Either[L, R])(handler: L => Any): R = {
val e = f
e.fold(l => { handler(l); retry(f)(handler) }, identity)
}
我认为 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 }
这是使用 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]
是一个单子结构,最终 return 是一个 A
。它使用蹦床来(通常)避免堆栈溢出。它还处理异常,并且可以重新抛出异常,或者通过 \/
(scalaz 的右偏 Either
)表示异常。
handleWith
允许为由 Task
引发的 Throwable
编写处理程序。这个处理程序的结果是一个新的 Task
到 运行 之后。在这种情况下,我们将只打印一条错误消息,并使用 flatMap
再次调用原来的 Task
。由于 Task
是一个蹦床结构,因此 应该 是安全的。
尝试使用 readInt.run
- 这将 运行 当前线程上的任务,并最终 return 传入 Int 值。
可以在此处找到重试 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)
}