Is there a functional/Scala way to call a function repeatedly until it succeeds, while reacting to failed attempts?
Let me illustrate with an example. Suppose I want to read an integer from standard-in, and retry if the user did not in fact enter an integer.
Given this function:
def read_int(): Either[String, Int] = {
val str = scala.io.StdIn.readLine()
try {
Right(str.trim().toInt)
} catch {
case _: java.lang.NumberFormatException => Left(str)
}
}
And this anonymous functions:
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)
}
I would use them like this:
val num = retry_until_right(ask_for_int)(handle_not_int)
println(s"Thanks! You entered: $num")
My questions are:
retry_until_right
already exist in Scala?Thanks!
*) Apart from the snake_case. I really like it.
def retry[L, R](f: => Either[L, R])(handler: L => Any): R = {
val e = f
e.fold(l => { handler(l); retry(f)(handler) }, identity)
}
I think the Try
monad together with the Iterator.continually
method is suitable for this general problem. Of course, this answer could be adopted to use Either
if you are so inclined:
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
Then you can do:
retry { Try(scala.io.StdIn.readLine.toInt) }{ _ => println("failed!") }
Or you can even hide the Try
part of the implementation, and give onWrong
a default value, and make it a second parameter instead of a curried function:
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
So then you can simply:
retry { scala.io.StdIn.readLine.toInt } { _ => println("failed") }
Or
retry { scala.io.StdIn.readLine.toInt }
Here's an alternative solution using 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)
}
}
And a retry wrapper (a bit less flexible):
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]
is a monadic structure that eventually returns a A
. It uses a trampoline to (usually) avoid stack overflows. It also handles exceptions, and can either rethrow the exception, or represent the exception via \/
(scalaz's right-biased Either
).
handleWith
allows one to write a handler for a Throwable
which was raised by the Task
. The result of this handler is a new Task
to run afterwards. In this case, we will just print out an error message, and call the original Task
again using flatMap
. Since Task
is a trampolined construct, this should be safe.
Try this with readInt.run
- this will run the task on the current thread, and eventually return the Int value passed in.
This was my first tail-recursive implementation:
@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)
}
}
But wanting to re-use existing libraries, I then switched to this approach using iterators:
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
Which can be used like this:
val num = retry_until_right(ask_for_int()) { str =>
println("Ivalid input: " + str)
}
println("Thanks! You entered: " + num)
However, it could be argued that hiding the Iterator
from plain view inside the implementation can be inflexible. What if the developer wants to perform further operations on it? (mapping, etc.)
Instead, the operations that react on "wrong" values and finally select the first "right" one can be abstracted into an "extension" class specific for iterators of the Either[L,R]
type, like this:
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
}
}
Now we can comfortably use the needed methods, in succinct code, while retaining control of the Iterator
like this:
val num = Iterator.continually(ask_for_int()).onLeft(handle_not_int).firstRight
println("Thanks! You entered: " + num)
Though I'm happy with this approach, I still wonder if this is not part of an existing library already...
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With