Searching online for the answer gives two prominent posts (Codacy's and Daniel Westheide's), and both give the same answer as Scala's official documentation for Try:
An important property of Try shown in the above example is its ability to pipeline, or chain, operations, catching exceptions along the way.
The example referenced above is:
import scala.io.StdIn
import scala.util.{Try, Success, Failure}
def divide: Try[Int] = {
val dividend = Try(StdIn.readLine("Enter an Int that you'd like to divide:\n").toInt)
val divisor = Try(StdIn.readLine("Enter an Int that you'd like to divide by:\n").toInt)
val problem = dividend.flatMap(x => divisor.map(y => x/y))
problem match {
case Success(v) =>
println("Result of " + dividend.get + "/"+ divisor.get +" is: " + v)
Success(v)
case Failure(e) =>
println("You must've divided by zero or entered something that's not an Int. Try again!")
println("Info from the exception: " + e.getMessage)
divide
}
}
But I can just as easily pipeline operations using the conventional try
block:
def divideConventional: Int = try {
val dividend = StdIn.readLine("Enter an Int that you'd like to divide:\n").toInt
val divisor = StdIn.readLine("Enter an Int that you'd like to divide by:\n").toInt
val problem = dividend / divisor
println("Result of " + dividend + "/"+ divisor +" is: " + problem)
problem
} catch {
case (e: Throwable) =>
println("You must've divided by zero or entered something that's not an Int. Try again!")
println("Info from the exception: " + e.getMessage)
divideConventional
}
(Note: divide
and divideConventional
differ slightly in behavior in that the latter errors out at the first sign of trouble, but that's about it. Try entering "10a" as input to dividend
to see what I mean.)
I'm trying to see scala.util.Try
's pipelining advantage, but to me it seems the two methods are equal. What am I missing?
I think you're having difficulty seeing the composition abilities of Try[T]
because you're handling exceptions locally in both cases. What if you wanted to compose divideConventional
with an additional operation?
We'd having some like:
def weNeedAnInt(i: Int) = i + 42
Then we'd have something like:
weNeedAnInt(divideConventional())
But let's say you want to max out the number of retries you allow the user to input (which is usually what you have in real life scenarios, where you can't re-enter a method forever? We'd have to additionally wrap the invocation of weNeedAnInt
itself with a try-catch
:
try {
weNeedAnInt(divideConventional())
} catch {
case NonFatal(e) => // Handle?
}
But if we used divide
, and let's say it didn't handle exceptions locally and propogated the internal exception outwards:
def yetMoreIntsNeeded(i: Int) = i + 64
val result = divide.map(weNeedAnInt).map(yetMoreIntsNeeded) match {
case Failure(e) => -1
case Success(myInt) => myInt
}
println(s"Final output was: $result")
Is this not simpler? Perhaps, I think this has some subjectivity to the answer, I find it cleaner. Imagine we had a long pipeline of such operations, we can compose each Try[T]
to the next, and only worry about the problems when once the pipeline completes.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With