Reputation: 9408
I have several blocks of code that follow this pattern:
// Dummy function defs.
def result(i : Int, d : Double, b : Boolean) = {
if (b) d else i
}
def fA(s : String) = {7}
def fB(s : String, i : Int) = {1.0}
def fC(s : String, i : Int, d : Double) = {true}
// Actual code.
def test(s : String) : Double = {
try {
val a = fA(s)
try {
val b = fB(s, a)
try {
val c = fC(s, a, b)
result(a, b, c)
} catch {
case _ => result(a, b, false)
}
} catch {
case _ => result(a, 0.0, false)
}
} catch {
case _ => result(0, 0.0, false)
}
}
Where a, b, & c are calculated in turn by the corresponding functions and then the values are passed to the result function. If at any stage an exception occurs then a default value is used in place of the remaining variables.
Is there a more idiomatic way to express this code. It reminds me of Monads in that it's a series of chained computations which bail out immediately if any computation fails.
Upvotes: 8
Views: 2196
Reputation: 1325
I generally solve most try-catch problems using a combination of map + recover
or flatMap + recoverWith
.
In general, I prefer chaining over nesting
There are many ways to skin a cat. Here are 3:
I presume fA, fB and fC are contrived examples of more complex logic. If they are indeed as noddy as the example then option 3 is simplest.
def test(s: String): Double =
(for {
a <- Try(fA(s)).recover { case _ => 0 }
b <- Try(fB(s, a)) recover { case _ => 0.0 }
c <- Try(result(a, b, fC(s, a, b))) recover { case _ => result(a, b, false) }
} yield c).get
def test(s: String): Double =
Try(fA(s)).recover { case _ => 0 }
.flatMap(a =>
(Try(fB(s, a)) recover { case _ => 0.0 })
.flatMap(b =>
Try(result(a, b, fC(s, a, b)))
recover { case _ => result(a, b, false) }
)
).get
Readability is not great because args into flatMap are written as anonymous functions. If I rewrite this without using anon functions then I have:
def test(s: String): Double = {
val evalA = Try(fA(s)).recover { case _ => 0 }
val evalB: Int => Try[Double] = a =>
Try(fB(s, a)) recover { case _ => 0.0 }
val evalC: (Int, Double) => Try[Double] = (a,b) =>
Try(result(a, b, fC(s, a, b))) recover { case _ => result(a, b, false) }
evalA.flatMap(a =>
evalB(a).flatMap(b =>
evalC(a, b))
).get
}
getOrElse()
if no other side-effects required.def test(s: String) = {
val evalA = Try(fA(s)).getOrElse(0)
val evalB = Try(fB(s, evalA)).getOrElse(0.0)
val evalC = Try(fC(s, evalA, evalB)).getOrElse(false)
result(evalA, evalB, evalC)
}
Upvotes: 2
Reputation: 3901
These types of problems are just what Try
aims to solve a bit more monadically (than nested try/catch
blocks).
Try
represents a computation that may either result in an exception, or return a successfully computed value. It has two subclasses for these-- Success
and Failure
.
Very funny that this question popped up when it did-- a few days ago, I finished up some additions and refactoring to scala.util.Try
, for the 2.10 release and this SO question helps to illustrate an important use-case for a combinator that we eventually decided to include; transform
.
(As of writing this, transform
is currently in the nightly and will be in Scala from 2.10-M5 onward, due out today or tomorrow. More info about Try
and usage examples can be found in the nightly docs)
With transform
(by nesting them), this can be implemented using Try
s as follows:
def test(s: String): Double = {
Try(fA(s)).transform(
ea => Success(result(0, 0.0, false)), a => Try(fB(s, a)).transform(
eb => Success(result(a, 0.0, false)), b => Try(fC(s, a, b)).transform(
ec => Success(result(a, b, false)), c => Try(result(a, b, c))
)
)
).get
}
Upvotes: 5
Reputation: 467
By defining those utility functions
implicit def eitherOps[E, A](v: Either[E, A]) = new {
def map[B](f: A => B) = v match {
case Left(e) => Left(e)
case Right(a) => Right(f(a))
}
def flatMap[B](f: A => Either[E, B]) = v match {
case Left(e) => Left(e)
case Right(a) => f(a)
}
def or(a: A) = v match {
case Left(_) => Right(a)
case x => x
}
}
def secure[A, B](f: A => B) = new {
def run(a: A): Either[Trowable, B] = try {
Right(f(a))
} catch {
case e => Left(e)
}
}
and simplifying yours
def fA(s : String) = 7
def fB(i : Int) = 1.0
def fC(d : Double) = true
We'll have:
def test(s: String): Either[Throwable, Double] = for {
a <- secure(fA).run(s).or(0)
b <- secure(fB).run(a).or(0.0)
c <- secure(fC).run(b).or(false)
} yield result(a, b, c)
Edit
Here's an executable but sadly, more verbose code snippet
object Example {
trait EitherOps[E, A] {
def self: Either[E, A]
def map[B](f: A => B) = self match {
case Left(e) => Left(e)
case Right(a) => Right(f(a))
}
def flatMap[B](f: A => Either[E, B]) = self match {
case Left(e) => Left(e)
case Right(a) => f(a)
}
def or(a: A) = self match {
case Left(_) => Right(a)
case x => x
}
}
trait SecuredFunction[A, B] {
def self: A => B
def secured(a: A): Either[Throwable, B] = try {
Right(self(a))
} catch {
case e => Left(e)
}
}
implicit def eitherOps[E, A](v: Either[E, A]) = new EitherOps[E, A] {
def self = v
}
implicit def opsToEither[E, A](v: EitherOps[E, A]) = v.self
implicit def secure[A, B](f: A => B) = new SecuredFunction[A, B]{
def self = f
}
def fA(s : String) = 7
def fB(i : Int) = 1.0
def fC(d : Double) = true
def result(i : Int, d : Double, b : Boolean) = {
if (b) d else i
}
def test(s: String): Either[Throwable, Double] = for {
a <- (fA _).secured(s) or 0
b <- (fB _).secured(a) or 0.0
c <- (fC _).secured(b) or false
} yield result(a, b, c)
}
Upvotes: 3
Reputation: 3608
I changed the example to use monads:
def fA(s: String) = Some(7)
def fB(i: Option[Int]) = Some(1.0)
def fC(d: Option[Double]) = true // might be false as well
def result(i: Int, d: Double, b: Boolean) = {
if (b) d else i
}
def test(s: String) = result(fA(s).getOrElse(0), fB(fA(s)).getOrElse(0.0), fC(fB(fA(s))))
Note: The for-comprehension is interpreted as chained flatMap
. So the type of res
is Option[(Int, Double, Boolean)]
. Therefore there is no need to write map
or flatMap
by yourself. The compiler does the work for you. :)
Edit
I edited my code to make it fit to all possibilitys. I will improve it, if I find a better way. Thank you for all your comments.
Upvotes: 4
Reputation: 41646
I'm not sure you can use monads as at each step you have two alternatives (exception or result) and to be faithful to your original code, on exception you don't want to be calling the fB
or fC
functions.
I was not able to elegantly remove the duplication of default values so I left it as I think it's clearer. Here is my non-monadic version based on either.fold
and control.Exception
:
def test(s : String) = {
import util.control.Exception._
val args =
allCatch.either(fA(s)).fold(err => (0, 0.0, false), a =>
allCatch.either(fB(s, a)).fold(err => (a, 0.0, false), b =>
allCatch.either(fC(s, a, b)).fold(err => (a, b, false), c =>
(a, b, c))))
(result _).tupled(args)
}
Upvotes: 5
Reputation: 1868
The previous answers seem to miss the fact that you want default result at each level. No need to be fancy with for expression here, you just need an helper function:
def optTry[T]( f: => T) : Option[T] = try { Some(f) } catch { case e:Exception => None }
OK, optTry
is a bad name (I'm not good at that game), but then, you can just:
def test(s : String) : Double = {
val a = optTry(fA(s)) getOrElse 0
val b = optTry(fB(s,a)) getOrElse 0.0
val c = optTry(fC(s,a,b)) getOrElse false
result(a,b,c)
}
Notice that Scala 2.10 will have a Try
data structure that basically does the same thing with a pimped Either
in place of Option
, see: http://www.scala-lang.org/archives/downloads/distrib/files/nightly/docs/library/index.html#scala.util.Try
Also notice that try { ... } catch { case _ => ... }
is a bad idea, you certainly don't want to catch some system Exception like OutOfMemory and the like.
EDIT: also, see Scalaz Validation
data structure for a world of awe for all that kind of problems. See: http://scalaz.googlecode.com/svn/continuous/latest/browse.sxr/scalaz/example/ExampleValidation.scala.html
Upvotes: 0
Reputation: 10571
You can use the catching
idiom as follows:
import scala.util.control.Exception._
def test(s : String) : Double = result(
catching(classOf[Exception]).opt( fA(s) ).getOrElse(0),
catching(classOf[Exception]).opt( fB(s, a) ).getOrElse(0.0),
catching(classOf[Exception]).opt( fC(s, a, b) ).getOrElse(false)
)
However, similarly to other solutions, this does make a slight executional change in that fB
and fC
will always be evaluated, whereas your original code only evaluates them if the prior calls succeeded.
Upvotes: 1
Reputation: 2738
Tried to make it more functional. Not sure if this solution is clearer than yours, but I think it shoud fit better if you'll have more computation steps.
def result(i : Int, d : Double, b : Boolean) = {
if (b) d else i
}
def fA(s : String) = {7}
def fB(s : String, i : Int) = {1.0}
def fC(s : String, i : Int, d : Double) = {true}
type Data = (Int, Double, Boolean)
def test(s : String) : Double = {
val steps = Seq[Data => Data](
{case (_, b, c) => (fA(s), b, c)},
{case (a, _, c) => (a, fB(s, a), c)},
{case (a, b, _) => (a, b, fC(s, a, b))}
)
val defaults: Either[Data, Data] = Right((0, 0.0, false))
val resultData = steps.foldLeft { defaults } { (eith, func) =>
eith match {
case left: Left[_,_] => left
case Right(data) => try {
Right(func(data))
} catch {
case _ => Left(data)
}
}
} fold (identity, identity)
(result _) tupled (resultData)
}
Upvotes: 0