Frage

Let's say I'm using a Java library from my Scala project. That Java library throws exceptions all over the place, but I don't feel comfortable just letting them propagate "in the Scala world", since there's no way to be sure what exceptions a Scala method can throw (except by documenting them). So this is the kind of code I tend to write:

def doesNotThrowExceptions(parameter: String): Either[Throwable, T] =
  catching(classOf[IOException], classOf[NoSuchAlgorithmException]) either {
    // Code calling the Java library
    // Code generating a value of type T
  }

Then, typically, I'll use Either.RightProjection.flatMap to chain methods that return Either[Throwable, ...] or Either.RightProjection.map to mix methods that return Either[Throwable, ...] and other methods. Or simply Either.fold to do something with the Throwable value. Somehow, though, this still doesn't feel totally right.

Is this the most "idiomatic" way to deal with Java's exceptions in Scala? Isn't there a better way?

War es hilfreich?

Lösung

I'm not sure there's a single most idiomatic way to deal with Java's exceptions because there are at least four different cases they'll get thrown:

  1. Something that neither you nor the library designer really expected to go wrong went wrong.
  2. Something that the library designer hoped would work didn't, and you need to know the details.
  3. Something you hoped would work didn't, and you don't need to know the details.
  4. Sometimes the method produces a value and sometimes not, and it communicates that by throwing an exception.

Best practices arguably differ for each of these cases

1. Truly exceptional exceptions

Scala has fully-featured exception handling. There is nothing wrong with letting an unanticipated exception propagate uncaught until you're at a level where you can do something about it. Packaging every possible exception into an Either can waste a lot of time. Just document what you know you're not handling, and use try/catch at an appropriately high level (e.g. a saveEverything method should probably go in a try/catch block (or wrap its contents in one) because no matter what went wrong, if saving everything failed you probably want to attempt to salvage the situation, not just die).

In particular, you probably want to handle Error this way and only package Exception, not all Throwables, into Either.

2. Exceptions that you need to know about

This is the case you're talking about, and you've already given several good suggestions for how to deal with them. As you've already noticed, you can use catching to package the exceptions into Either. You then can also

a. Use pattern matching, which will let you pull apart your Either in more depth:

doesNotThrowExceptions("par").right.map(transformData) match {
  case Left(ioe: IOException) => /* ... */
  case Left(nsae: NoSuchAlgorithmException) => /* ... */
  case Right(x) => /* ... */
  case Left(e) => throw e  // Didn't expect this one...
}

b. Downconvert to Option after logging the error:

doesNotThrowExceptions("par").left.map{ e =>
  println("You're not going to like this, but something bad happened:")
  println(e)
  println("Let's see if we can still make this work....")
}.right.toOption

c. If exceptions are a really important part of your flow-control, Either may not be enough. You might instead want to define your own Either-like class with more than just Left and Right. Or you can nest the left side of the Either:

try { Right(/* java code */) }
catch {
  case ioe: IOException => Left(Left(ioe))
  case nsae: NoSuchAlgorithmException => Left(Right(nsae))
}

d. Use Scalaz Validation, which is a lot like Either but is tailored a little more to exception handling.

3. Exceptions where you only need to know something went wrong, and

4. Exceptions thrown to indicate no-return-value

Even though these are conceptually two different categories, you handle them the same way:

catching(classOf[IOException], classOf[NoSuchAlgorithmException]) opt { ... }

to get an Option back. Then map, flatMap, etc..

Andere Tipps

I always prefer scalaz.Validation over scala.Either. The two are algebraically identical structures, but the former is functionally richer (has more useful methods and type class instances).

Check this link for a great presentation by Chris Marshall on scalaz.Validation and other Scalaz goodies.

In order to talk to the code that uses classic exception handling mechanism, I have defined the following enriching methods and a type class instance:

implicit def catchW[A](underlying: Catch[A]) = new CatchW(underlying)

class CatchW[A](underlying: Catch[A]) {
  def validation(body: => A): Validation[Throwable, A] = {
    underlying.withApply(_.fail)(body.success)
  }

  def vnel(body: => A): ValidationNEL[Throwable, A] = {
    underlying.withApply(_.failNel)(body.success)
  }
}

implicit def catchMonoid[A] = new Monoid[Catch[A]] {
  val zero = noCatch
  def append(x: Catch[A], y: => Catch[A]) = x or y
}
Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top