0

This is a follow-up for my previous question.

Suppose I have functions foo, bar, and qux that raise exceptions.

def foo(x: Int, y: Int, z: Int): String = ...
def bar(s: String): String = ...
def qux(t: String): String = ...

I am composing them as follows:

def myFun(x: Int, y: Int, z: Int): String = {
  val s = foo(x, y, z)
  val t = bar(s)
  qux(t)
}

What if bar raises an exception ? If the exception does not contain s I cannot understand what happened. I probably need to add s to that exception. (The same applies to Either, \/, etc.)

So I am defining an exception, which "captures" the arguments:

case class MyException(e: Exception, args: Any*) extends Exception(e)

and writing myFun as follows:

def myFun(x: Int, y: Int, z: Int): String = {

  def myFoo(x: Int, y: Int, z: Int) = try foo(x, y, z) catch { 
    case NonFatal(e: Exception) => throw MyException(e, x, y, z)
  }

  def myBar(s: String) = try bar(s) catch { 
    case NonFatal(e: Exception) => throw MyException(e, s)
  }

  def myQux(t: String) = try qux(t) catch { 
    case NonFatal(e: Exception) => throw MyException(e, t)
  }

  val s = myFoo(x, y, z)
  val t = myBar(s)
  myQux(t)
}

I have added a lot of code now but I can catch the exception and print it out to see the data I need.

Does it make sense ? Is it possible to get rid of the boilerplate code in myFoo, myBar, myQux ? Does Shapeless help here?

Community
  • 1
  • 1
Michael
  • 41,026
  • 70
  • 193
  • 341

1 Answers1

2

I think you are trying to patch a deeper issue here. In principle, foo, bar, and qux should raise meaningful exceptions. Joshua Bloch in his book "Effective Java" extends on this point where meaningful Exceptions should be constructed with the parameters that help track back the issue at hand.

Imagine this principle applied to your question:

def foo(a:Int, b:Int, c:Int):String = if (a<0) throw new ParameterOutOfBounds("a","lt 0") else s"a->$a,b->$b,c->$c"
def bar(s:String):String = if (s.endsWith("0")) throw IllegalArgumentException(s"Unacceptable suffix ${s.last}") else s
def qux(s:String):String = if (s.endsWith("1")) throw IllegalArgumentException(s"Unacceptable suffix ${s.last}") else s

You can compose these functions and still preserve any Exception on the composition:

def composed(a:Int, b:Int, c:Int):Try[String] = 
for { vfoo <- Try(foo(a,b,c)); 
      vbar <- Try(bar(vfoo)); 
      vqux <- Try(qux(vfoo))}  
yield (vqux)

scala> composed(1,2,3)
res4: scala.util.Try[String] = Success(a->1,b->2,c->3)

scala> composed(-1,2,3)
res7: scala.util.Try[String] = Failure(ParameterOutOfBounds: Invalid Parameter=a.   Reason=lt 0)


scala> composed(1,2,0)
res5: scala.util.Try[String] = Failure(java.lang.IllegalArgumentException: Unacceptable suffix 0)

In case foo,bar and qux are code you cannot modify, I'd wrap those exceptions using the same principle of "something meaningful" instead of packaging their parameters in a generic exception.

maasg
  • 37,100
  • 11
  • 88
  • 115
  • Thanks, but what if the function I am calling _do not_ add their arguments to the exceptions ? I still need a feasible solution in this case. – Michael May 14 '14 at 10:41
  • 1
    Arguments alone are not very informative. I tried to illustrate that in the example above. If `bar` and `qux` both would only throw `Exception`, the parameters (the same in this simplified case) won't tell you why the exception happened. You may want to wrap these functions with something specific to them that improves their Exception handling. – maasg May 14 '14 at 12:11
  • otherwise, consider logging as a less-intrusive alternative to your class structure. – maasg May 14 '14 at 14:16