Why following code
def doSomething() = "Something"
var availableRetries: Int = 10
def process(): String = {
while (true) {
availableRetries -= 1
try {
return doSomething()
} catch {
case e: Exception => {
if (availableRetries < 0) {
throw e
}
}
}
}
}
produces following compiler error
error: type mismatch;
found : Unit
required: String
while (true) {
^
?
This works ok in C#. The while loops forever, so it cannot terminate, therefore it cannot result something else than string. Or how to make infinite loop in Scala?
Unlike C# (and Java and C and C++) which are statement based languages, Scala is an expression based language. That's mostly a big plus in terms of composibility and readability but in this case the difference has bitten you.
A Scala method implicitly returns the value of the last expression in the method
scala> def id(x : String) = x
id: (x: String)String
scala> id("hello")
res0: String = hello
In Scala pretty much everything is an expression. Things that look like statements are still expressions that return a value of a type called Unit. The value can be written as ().
scala> def foo() = while(false){}
foo: ()Unit
scala> if (foo() == ()) "yes!" else "no"
res2: java.lang.String = yes!
No compiler for a Turing-equivalent language can detect all non-terminating loops (c.f. Turing halting problem) so most compilers do very little work to detect any. In this case the type of "while(someCondition){...}" is Unit no matter what someCondition is, even if it's the constant true.
scala> def forever() = while(true){}
forever: ()Unit
Scala determines that the declared return type (String) isn't compatible with the actual return type (Unit), which is the type of the last expression (while...)
scala> def wtf() : String = while(true){}
<console>:5: error: type mismatch;
found : Unit
required: String
def wtf() : String = while(true){}
Answer: add an exception at the end
scala> def wtfOk() : String = {
| while(true){}
| error("seriously, wtf? how did I get here?")
| }
wtfOk: ()String
Functional way to define an infinite loop is recursion:
@annotation.tailrec def process(availableRetries: Int): String = {
try {
return doSomething()
} catch {
case e: Exception => {
if (availableRetries < 0) {
throw e
}
}
}
return process(availableRetries - 1)
}
elbowich's retry
function without inner loop
function:
import scala.annotation.tailrec
import scala.util.control.Exception._
@tailrec def retry[A](times: Int)(body: => A): Either[Throwable, A] = {
allCatch.either(body) match {
case Left(_) if times > 1 => retry(times - 1)(body)
case x => x
}
}
The compiler isn't smart enough to know that you can't exit the while loop, unfortunately. It's easy to trick, though, even if you can't sensibly generate a member of the return type--just throw an exception.
def process(): String = {
while (true) {
...
}
throw new Exception("How did I end up here?")
}
Now the compiler will realize that even if it escapes the while loop, it can't return a value there, so it doesn't worry that the while loop has return type Unit
(i.e. does not return a value).
import scala.annotation.tailrec
import scala.util.control.Exception._
def retry[A](times: Int)(body: => A) = {
@tailrec def loop(i: Int): Either[Throwable, A] =
allCatch.either(body) match {
case Left(_) if i > 1 => loop(i - 1)
case x => x
}
loop(times)
}
retry(10) {
shamelessExceptionThrower()
}
edit: I just noticed the actual return statement. The return statement inside the while loop will be ignored. For example, in the REPL:
scala> def go = while(true){return "hi"}
<console>:7: error: method go has return statement; needs result type
def go = while(true){return "hi"}
^
You told the compiler that the process()
method returns a String
, but your method body is just a while
loop, which doesn't return anything (it's a Unit
, or a Java void
). Either change the return type or add a String after the while loop.
def process(): Unit = {
while(true){...}
}
or
def process(): String = {
while(true){...}
"done"
}
Based on senia, elbowich and dave's solutions I used following:
@annotation.tailrec
def retry[T](availableRetries: Int)(action: => T): T = {
try {
return action
} catch {
case e: Exception if (availableRetries > 0) => { }
}
retry(availableRetries - 1)(action)
}
Which can be then used as elbowich and dave's solutions:
retry(3) {
// some code
}