Catching an exception within a map
What is the be开发者_StackOverflow中文版st way of handling exceptions while iterating over a loop in Scala?
For instance, if I had a convert()
method that could throw an exception, I'd like to catch that exception, log it, and keep iterating. Is there a "scala" way to do this?
Ideally, I'd like something like...
val points: Seq[Point] = ...
val convertedPoints: Seq[ConvertedPoint] = points.map(
p => {
try { p.convert() }
catch { case ex: Exception => logger.error("Could not convert", ex) }
})
You can't do the above code since it's not a direct mapping from one list to the other (you get back Seq[Any]
as opposed to Seq[ConvertedPoint]
).
flatMap is probably what you're looking for, but the map function has logging side-effect and these side-effects may not occur immediately if points were a view:
val convertedPoints = points.view.flatMap { p =>
try {
Some(p.convert)
} catch {
case e : Exception =>
// Log error
None
}
}
println("Conversion complete")
println(convertedPoints.size + " were converted correctly")
This would print:
Conversion complete
[Error messages]
x were converted correctly
In your case, drop the view and you're probably fine. :)
To make the conversion a pure function (no side-effects), you'd probably use Either. Although I don't think it's worth the effort here (unless you actually want to do something with the errors), here's a pretty complete example of using it:
case class Point(x: Double, y: Double) {
def convert = {
if (x == 1.0) throw new ConversionException(this, "x is 1.0. BAD!")
else ConvertedPoint(x, y)
}
}
case class ConvertedPoint(x: Double, y: Double)
class ConversionException(p: Point, msg: String) extends Exception(msg: String)
val points = List(Point(0,0), Point(1, 0), Point(2,0))
val results = points.map { p =>
try {
Left(p.convert)
} catch {
case e : ConversionException => Right(e)
}
}
val (convertedPoints, errors) = results.partition { _.isLeft }
println("Converted points: " + convertedPoints.map(_.left.get).mkString(","))
println("Failed points: " + errors.map( _.right.get).mkString(","))
Interesting that I had a lot of trouble explaining the advantages of using scala.util.control.Exception
over try
/catch
, and then I start to see questions that make perfect examples out of them.
Here:
import scala.util.control.Exception._
List(1, 23, 5, 2, 0, 3, 2) flatMap (x => catching(classOf[Exception]) opt (10 / x))
Your own code would look like this:
val points: Seq[Point] = ...
val convertedPoints: Seq[ConvertedPoint] = points.flatMap(
p => handling(classOf[Exception]) by { ex =>
logger.error("Could not convert", ex); None
} apply Some(p.convert)
)
Or, if you refactor it:
val exceptionLogger = handling(classOf[Exception]) by { ex =>
logger.error("Could not convert", ex); None
}
val convertedPoints: Seq[ConvertedPoint] = points.flatMap(p => exceptionLogger(Some(p.convert)))
Perhaps you want a flatMap. Here is an example, should see how it can fit :-)
List(1,2,3,4).flatMap(x => if (x > 2) Some(x) else None)
The above would use side-effect logging (printing or putting in something mutable -- if this is done, make sure the evaluation is forced!). To get around the side-effects and caveats, the mapping function could just be of Point -> Either[CovertedPoint,Exception]
and then the results can be separated with Seq.partition
or similar.
Starting Scala 2.10
you could use Try
to catch exceptions with flatMap
to get rid of these and starting Scala 2.13
couple Try
with tap
to log exceptions:
List("34", "a", "1", "3", "1l")
.flatMap(x => Try(x.toInt).tap(_.failed.foreach(println)).toOption)
// java.lang.NumberFormatException: For input string: "a"
// java.lang.NumberFormatException: For input string: "1l"
// res0: List[Int] = List(34, 1, 3)
This:
map
s each element to anInt
(our example of a code generating an exception) safe guarded by wrapping it in aTry
. This will produce aTry[Int]
which is either aFailure(NumberFormatException)
or aSuccess(12)
.These
Try
s aretap
ped toprint
(or log) the error for theFailure
s.tap
applies a side effect (in this case some logging) on any value while returning the original value, so the returned value oftap
is the element it's applied on, i.e. the unmodifiedTry
.We then transform the
Try
s intoOption
s (Success(12)
becomesSome(12)
andFailure(NumberFormatException)
becomesNone
) in order to apply aflatMap
which get rids ofNone
s (Failure
s) and extract values (12
fromSome(12)
(Success(12)
)).
Before Scala 2.13
, an equivalent version without tap
could be:
List("34", "a", "1", "3", "1l")
.flatMap(x =>
(Try(x.toInt) match {
case f @ Failure(e) => {
println(e)
f
}
case s => s
}).toOption
)
精彩评论