Scala local return?

2019-05-04 13:26发布

问题:

I've just discovered that returns in following closure will return from function findPackage

def findPackage(name: String, suffix: Option[String] = None): Path = {
    logger.debug("Looking for package {} with suffix {}", name, suffix)
    val path: Path = using(Files.newDirectoryStream(appDir)) {dirs =>
        for (val path <- dirs) {
            val matcher = packagePattern.matcher(path.getFileName.toString)
            if (matcher.matches() && matcher.group(1).equals(name))
                if (suffix.isDefined) {
                    if (matcher.group(2) != null && matcher.group(2).equals(suffix.get))
                        return path
                } else
                    return path
        }
        throw new PackageNotFoundException(this, name, suffix)
    }
    logger.debug("Found package is {}", path)
    path
}

Can I somehow do a local return please? Thank you.

回答1:

Or you could get rid of your loop and replace it with what you're trying to do: "find"

def findPackage(name: String, suffix: Option[String] = None): Path = {
    logger.debug("Looking for package {} with suffix {}", name, suffix)

    def matching(path : Path) : Boolean = {
        val matcher = packagePattern.matcher(path.getFileName.toString)
        matcher.matches && matcher.group(1).equals(name) && (!suffix.isDefined || (matcher.group(2) != null && matcher.group(2).equals(suffix.get))
    }

    val path: Path = using(Files.newDirectoryStream(appDir)) {dirs =>
       dirs find matching getOrElse {throw new PackageNotFoundException(this, name, suffix)}
    }

    logger.debug("Found package is {}", path)
    path
}


回答2:

I fully support James Iry's suggestion, but for the sake of demonstration:

def findPackage(name: String, suffix: Option[String] = None): Path = {
    logger.debug("Looking for package {} with suffix {}", name, suffix)
    val path: Path = using(Files.newDirectoryStream(appDir)) {dirs =>
        try {
          for (val path <- dirs) {
              val matcher = packagePattern.matcher(path.getFileName.toString)
              if (matcher.matches() && matcher.group(1).equals(name))
                  if (suffix.isDefined) {
                      if (matcher.group(2) != null && matcher.group(2).equals(suffix.get))
                          return path
                  } else
                     return path
          }
          throw new PackageNotFoundException(this, name, suffix)
        } catch { case e:scala.runtime.NonLocalReturnControl[Path] => e.value}
    }
    logger.debug("Found package is {}", path)
    path
}

What changed?

I have added a try{} block around the body of the anonymous function and then catch expression at the end looking for scala.runtime.NonLocalReturnControl exception, then I extract and pass on the return value.

Why it works?

Returning from a nested anonymous function raises scala.runtime.NonLocalReturnControl exception which is caught by the host function or method.

Scala Language Spec, section 6.20 Return Expressions:

... Returning from a nested anonymous function is implemented by throwing and catching a scala.runtime.NonLocalReturnException. Any exception catches between the point of return and the enclosing methods might see the exception. A key comparison makes sure that these exceptions are only caught by the method instance which is terminated by the return.

If the return expression is itself part of an anonymous function, it is possible that the enclosing instance of f has already returned before the return expression is executed. In that case, the thrown scala.runtime.NonLocalReturnException will not be caught, and will propagate up the call stack.



回答3:

Yes, you can, by defining a local method:

def findPackage(name: String, suffix: Option[String] = None): Path = {
    logger.debug("Looking for package {} with suffix {}", name, suffix)

    def search(dirs: List[File]) = { // not sure what the type of dirs actually is
        for (val path <- dirs) {
            val matcher = packagePattern.matcher(path.getFileName.toString)
            if (matcher.matches() && matcher.group(1).equals(name))
                if (suffix.isDefined) {
                    if (matcher.group(2) != null && matcher.group(2).equals(suffix.get))
                        return path
                } else
                    return path
        }
        throw new PackageNotFoundException(this, name, suffix)
    }

    val path: Path = using(Files.newDirectoryStream(appDir))(search _)
    logger.debug("Found package is {}", path)
    path
}

or by throwing some exception and catching it:

def findPackage(name: String, suffix: Option[String] = None): Path = {
    logger.debug("Looking for package {} with suffix {}", name, suffix)
    val path: Path = using(Files.newDirectoryStream(appDir)) {dirs =>
        try {
            for (val path <- dirs) {
                val matcher = packagePattern.matcher(path.getFileName.toString)
                if (matcher.matches() && matcher.group(1).equals(name))
                    if (suffix.isDefined) {
                        if (matcher.group(2) != null && matcher.group(2).equals(suffix.get))
                            throw new ReturnException(path)
                    } else
                        throw new ReturnException(path)
            }
            throw new PackageNotFoundException(this, name, suffix)
        }
        catch { case ReturnException(path) => path }
    }
    logger.debug("Found package is {}", path)
    path
}


回答4:

I had a similar trouble, and I solved it by propagating any ControlThrowable I might find, like it says here

def asJson: JsValue = {
  val key = "classification.ws.endpoint"

  configuration.getString(key).map{ url =>
    try {
      return WS.url(url).get().await.get.json
    } catch {
      case ce : scala.util.control.ControlThrowable => throw ce
      case e => throw InvalidWebServiceResponseException("Error accessing '%s'".format(url), e)
    }
  }.getOrElse {
    throw new MissingConfigurationException("No value found for '%s' configuration".format(key))
  }

}

Note that in this case, just removing the "return" statement solves the problem...