Scala while(true) type mismatch? Infinite loop in

2019-04-06 16:57发布

问题:

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?

回答1:

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


回答2:

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 
  } 
} 


回答3:

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).



回答4:

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()
}


回答5:

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"
}


回答6:

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
}