Thursday, September 29, 2016

Rethinking scala.util.Try - Better Alignment with the Principles of DbC

Version: 2016.10.01

Having used (scala.util.)Try pretty extensively for the last +2 years in my Geospatial Scala/Akka based SaaS project, BricksAndMortar (see http://www.qalocate.com), I’ve come to know the ins and outs of Try pretty well. Recently, I realized that I wanted to extend Try to make it more tightly scoped for the specific contracts I needed to represent when designing API entry points. I like the ideas behind Eiffel’s DbC (Design by Contract) and want to elevate the error/exception aspect of Scala to be more DbC aligned. (see https://en.wikipedia.org/wiki/Design_by_contract)

Said a slightly different way, I want to make the interface contract offered by Try to be more explicit and uniform. I want it to clearly state to the client what narrowing of (java.lang.)Throwable it clearly owns. And by implication, it is also clearly stating what it would not be handling and is left to a catch block somewhere higher up in the call chain. This is the essential nature of DbC; i.e. clearly letting the client of the class method know what is expected to be received (preconditions on the input parameters) and what the client can expect in return (postconditions about the values returned by the method). I am skipping the invariants aspect of DbC as they are more relevant for stateful (mutable) classes of which Try is not.


Table of Contents:

What Are Try’s Main Pain Points?

Proposed Alternative

Reviewing the Source for (scala.util.)Try

Achieving Goals 1 through 3 - Deeply Embracing DbC

Defining TryBase[T <: Throwable, +V]

Defining TryObjectBase[T <: Throwable]

Defining SuccessBase[T <: Throwable, +V]

Defining FailureBase[T <: Throwable, +V]

Defining TryRuntimeException

Defining TryThrowable and TryException

Achieving Goal 4 - Creating a Drop-in Replacement for Try

Achieving Goal 5 - Reducing Throwable Pathway Logging Boilerplate

Achieving Goal 6 - Optionally Capturing Enclosing Context for FailureBase

Conclusion


What Are Try’s Main Pain Points?

Inconveniences

  • Requires too much boilerplate for:
    • Filtering exceptions to a smaller scope for which the class or method has been designed
    • Emitting logging output in the face of generating a Failure and/or observing a fatal condition

Design issues

  • Doesn’t provide a means for capturing additional local context in a Failure facilitating a more fine-grained means to surface detailed information from a nested set of lower level Failures; i.e. Try is an exception/error catch all
  • Doesn’t enable allowing client to be a pass through for error conditions for which it cannot handle
    • This is seems important for dependency injection; especially as a middleman between a master framework and some specialized library where _some_ of the specialized library exceptions/errors need to flow back to the framework to be handled (for example, imagine JDBC connection pool on a web-server where that error is required to get the JDBC connection pool initialized or reset)

Most of the code I write, especially around ADTs (Abstract Data Types), only cares about generating and capturing RuntimeException and its descendants. So, the fact that Try is capturing instances well above RuntimeException in the of Throwable hierarchy (think Exception or Error) means I am having to constantly create boilerplate to extract the Throwable instance, check to make sure it was an instance of or descendent of RuntimeException, and if not rethrow it.

And it turns out, the current implementation of Try will catch my explicit rethrow and force it into a Failure anyway (excluding a small fatal sub-domain; see http://www.scala-lang.org/api/2.11.8/index.html#scala.util.control.NonFatal$). Not only is this an unexpected behavior not effectively described in the scaladoc, it clearly falls outside the assumed contract for the ADT.

Sidenote: As we progress in this article, you will notice I am using the terms “error/exception”, “java.lang.Throwable” and “Throwable” as I discuss the Try domain as opposed to simply the word “exception”. This is because one of the descendants of Throwable is Exception. So, using the word “exception” can make it ambiguous as to whether I am referring to Exception or Throwable. (see https://docs.oracle.com/javase/8/docs/api/index.html?java/lang/Throwable.html and http://docstore.mik.ua/orelly/java/langref/ch09_04.htm)

Additionally, catching a Throwable and rethrowing engages one of the areas of slowest performance execution within the JVM even with the Hotspot optimizations specific to this area (see https://shipilev.net/blog/2014/exceptional-performance).

And adding logging to the Failure/Throwable pathway requires another rash of boilerplate.

Finally, I would like to enable more context be provided at a particular Failure (not shown in Code Snippet #1). Without it, I attempt to force fit whatever relevant context I can muster into the single Throwable parameter the Failure offers. The result of this is the stench of a horrid code smell. An example of this particular pain point is creating a summarizing Failure by concatenating all of the messages from a collection of Failures after just processing a List.

To concretely visualize the Try boilerplate for narrowing to just RuntimeException (and descendants) and emitting logging output (but not including the force fit of relevant context into the single Failure from some upstream collection of Failures):


WWwT - Code Snippet #1: scala.util.Try boilerplate


import scala.util._

//...

def evaluateTheRiskyThing: Int =

  3/0

  //throw new Throwable("uhoh")

//...

val tryEvaluateTheRiskyThing1 =

  Try(evaluateTheRiskyThing).recoverWith {

    case runtimeException: RuntimeException =>

      println(s"Failure: ${runtimeException.getMessage}")

      Failure(runtimeException) //an additional instantiation

    case throwable: Throwable =>

      println(s"Uncaught: ${throwable.getMessage}")

      //Surprise!!! The rethrown throwable below gets

      //  re-caught by this Try method and

      //  turned into a Failure anyway! ARGH!

      throw throwable

  }

//...

val tryEvaluateTheRiskyThing2 =

  Try(evaluateTheRiskyThing) match {

    case failure @ Failure(throwable) =>

      throwable match {

        case runtimeException: RuntimeException =>

          println(s"Failure: ${runtimeException.getMessage}")

          failure

        case _ =>

          println(s"Uncaught: ${throwable.getMessage}")

          throw throwable //this rethrow actually works

    }

   case success @ _ =>

     success

 }


Sidenote: If you would like to explore this code without having to install a Scala IDE (or compiler), you can visit ScalaKata (http://www.scalakata.com) which offers an interactive web page for Scala compilation and application running. For the example code above, place the snippet inside the curly braces for @instrument class Playground. Then, hit the run arrow at the top of the bar on the right side of the page. It should show the types of the instances generated. And the println will appear in the upper left (just below the import com.scalakata._) showing “Failure: / by zero”. If you would like to see the other error, first press the X (which replaced the run button). Then, comment out the line 3/0 and uncomment out the line throw new Throwable(“uhoh”). Then press the run button again.

So, not only is there lots of additional boilerplate to filter to only the Throwable sub-domain about which I care (in this example, RuntimeException), there is the risk of, by default, making the code less performant with the additional catching and then rethrowing when the Throwable instance wasn’t one intended to be handled.

Eventually I tired of applying these patterns (as it felt like Java boilerplate land all over again) and eventually stopped using it. This means thanks to my being lazy, I now have code that is capturing Throwable instances where the local context has no intention of or ability to handle and should leave those instances to pass through and up the call chain.

And I use Try extensively across thousands of files through dozens of projects, so this means there could be serious errors/exceptions being thrown for which the local context is just silently suppressing them...which is EXACTLY what I don’t want to have happen. As a proactive response, it results in my generating copious logging around specific areas where I cannot afford the errors/exceptions to remain suppressed. That in turn infects even more code resulting in these huge logging dumps which become onerous to dig through attempting to find the relevant context to diagnose the root cause of some production runtime issue.


Proposed Alternative

I will generate an enhanced Try which has a much stronger design influenced by DbC, specifically on the Throwable side. And I will substantially ease, or even entirely remove, most of the boilerplate related to emitting logging.

The explicit goals I intend to achieve are:

  • Move the dependence upon Throwable to a generic parameter
    • Do so with minimal dependence upon the scala.reflect package to ensure Scala.js can still use the solution no modification
  • Reduce client boilerplate (see example above) when filtering to a narrowed descendant of Throwable
  • Alter the implementation to be more “pure” and consistent with DbC by:
    • Ensuring only narrowed Throwable instances are captured and all others are ignored or rethrown
    • Ensuring all methods with function input parameters clearly indicate the call to a function parameter will not catch ANY instance of Throwable; i.e. the function must manage on its own which Throwable instances fall within the DbC scope of its implementation (which could be narrow or wider than this instance of Try)
  • Reproduce a drop-in replacement of Try as a new entity set in the new framework; i.e. precisely mimics the contractual behavior of Try (surround application of function input parameters with a try/catch mapping catches to Failures)
    • Ease transition into this new model
  • Reduce client boilerplate for emitting logging (see example above)
  • Enable capture of enclosing context when producing a Failure; ease the process of producing a summarizing Failure from a List[Failure]


Reviewing the Source for (scala.util.)Try

Try is contained in a single and relatively short file consisting of these entities (see https://github.com/scala/scala/blob/v2.11.8/src/library/scala/util/Try.scala):

  1. Try[+T] - abstract class
  2. Try - explicit companion object
  3. Success[+T] - final case class
  4. Failure[+T] - final case class


Achieving Goals 1 through 3 - Deeply Embracing DbC

To achieve the first three goals, I will be defining 4 entities which strongly correlate to those in the Try file and will be named:

  1. TryBase[T <: Throwable, +V] - trait
  2. TryObjectBase[T <: Throwable] - abstract class
  3. SuccessBase[T <: Throwable, +V] - trait
  4. FailureBase[T <: Throwable, +V] - trait

Sidenote: I know it causes a bit of cognitive dissonance to change the meaning of T from Try to reattach it to V and then introduce the new generic parameter as T. However, one of the imperative goals of creating this solution is to ensure clients are very clear that we are Throwable as opposed to Exception (or any other descendant) and to keep it clearly distinct from the value being contained. Such is the cost of attempting to create a change intending to support intuitive reasoning for a client while still dealing with the baggage of the past.

After working through the definition of each of the above *Base entities, I’ll show how to compose concrete solutions; TryThrowable, TryException and TryRuntimeException, engaging all of the functionality in these *Base entities.

Defining TryBase[T <: Throwable, +V]

TryBase[T <: Throwable, +V] takes two generic parameters; T and V. The first generic parameter, T,  defines the class instance of the Throwable instance by which the catch clause filters. This includes the class itself and any of its descendants. The second generic parameter, V, is identical to the generic parameter, T, in Try; i.e. it represents the type of the value when the computation is successful.

There are minor differences, both in definition and implementation, from this new trait and the original in Try. We’ll more deeply explore those differences once we begin implementing the concrete entities which specifically reproducing Try itself (referred to as TryThrowableNonFatal). Also note there is a forward reference to trait TryObjectBase[T <: Throwable].

Here’s version 1.0 of the trait TryBase[T <: Throwable, +V] (achieves goals 1-6):


AG1t3 - Code Snippet #1: v1.0 trait TryBase[T <: Throwable, +V] 


trait TryBase [T <: Throwable, +V] {

  protected def tryObjectBase: TryObjectBase[T]

  //val cannot be used - initialization issues

  def isSuccess: Boolean //explicitly override to val in both concrete traits

  def v: V =

    throw new UnsupportedOperationException("v is undefined")

  //val cannot be used - initialization issues

  def isFailure: Boolean  //explicitly override to val in both concrete traits

  def t: T =

    throw new UnsupportedOperationException("t is undefined")

  def get: V

  def getOrElse[W >: V](default: => W): W

  def toOption: Option[V]

  def filter(p: V => Boolean): TryBase[T, V]

  def flatMap[W](f: V => TryBase[T, W]): TryBase[T, W]

  def flatten[W](implicit ev: V <:< TryBase[T, W]): TryBase[T, W]

  def map[W](f: V => W): TryBase[T, W]

  def transform[W](fv: V => TryBase[T, W], ft: T => TryBase[T, W]): TryBase[T, W]

  def orElse[W >: V](default: => TryBase[T, W]): TryBase[T, W]

  def recover[W >: V](f: PartialFunction[T, W]): TryBase[T, W]

  def recoverWith[W >: V](f: PartialFunction[T, TryBase[T, W]]): TryBase[T, W]

  def foreach[W](f: V => W): Unit

}


Defining TryObjectBase[T <: Throwable]

TryObjectBase[T <: Throwable] takes a single generic parameter, T (with exactly the same meaning as the same parameter for TryBase). This abstract class represents the TryBase’s companion object which encapsulates the only place performing the core try/catch logic.

This also abstracts the operations for creating instances of SuccessBase and FailureBase. It also adds filtering logic in the catch clause of the try to filter anything in the catch clause at the granularity of T (which is defaulted to Throwable).

Here’s version 0.3 of the abstract class TryObjectBase[T <: Throwable] (achieves goals 1-4):


AG1t3 - Code Snippet #2: v0.3 abstract class TryObjectBase[T <: Throwable] 


import scala.reflect.ClassTag

//unable to make a trait - won’t compile with ": ClassTag"

abstract class TryObjectBase[T <: Throwable : ClassTag] {

  def successT[V]: V => TryBase[T, V]

  def failureT[V]: T => TryBase[T, V]

  protected def isInstanceValid(t: Throwable): Boolean =

    true

  def apply[V](vToEvaluate: => V): TryBase[T, V] =

    try

      successT(vToEvaluate)

    catch {

      case t: T if isInstanceValid(t) =>

        failureT[V](t.asInstanceOf[T])

    }

}


Defining SuccessBase[T <: Throwable, +V]

The two SuccessBase[T <: Throwable, +V] generic parameters, T and V, have exactly the same meaning as the corresponding generic parameters for TryBase[T <: Throwable, +V]. This trait represents the results of a successful computation.

Here’s version 0.5 of the trait SuccessBase[T <: Throwable, +V] (achieves goals 1-6):


AG1t3 - Code Snippet #3: v0.5 trait SuccessBase[T <: Throwable, +V]


trait SuccessBase[T <: Throwable, +V] extends TryBase[T, V] {

  val isSuccess = //explicit override to val per parent specification

    true

  val isFailure = //explicit override to val per parent specification

    !isSuccess

  def get =

    v

  def getOrElse[W >: V](default: => W): W =

    get

  def toOption: Option[V] =

    Some(get)

  def filter(p: V => Boolean): TryBase[T, V] =

    if (p(v))

      this

    else

      tryObjectBase.failureT[V](

        new IllegalArgumentException(

          s"filter predicate does not hold for v [$v]"

        ).asInstanceOf[T]

      )

  def flatMap[W](f: V => TryBase[T, W]): TryBase[T, W] =

    f(v)

  def flatten[W](implicit ev: V <:< TryBase[T, W]): TryBase[T, W] =

    this.asInstanceOf[TryBase[T, W]]

  def map[W](f: V => W): TryBase[T, W] =

    tryObjectBase(f(v))

  def transform[W](fv: V => TryBase[T, W], ft: T => TryBase[T, W]): TryBase[T, W] =

    fv(v)

  def orElse[W >: V](default: => TryBase[T, W]): TryBase[T, W] =

    this.asInstanceOf[TryBase[T, W]]

  def recover[W >: V](f: PartialFunction[T, W]): TryBase[T, W] =

    this.asInstanceOf[TryBase[T, W]]

  def recoverWith[W >: V](f: PartialFunction[T, TryBase[T, W]]): TryBase[T, W] =

    this.asInstanceOf[TryBase[T, W]]

  def foreach[W](f: V => W): Unit =

    f(v)

}


Defining FailureBase[T <: Throwable, +V]

The two FailureBase[T <: Throwable, +V] generic parameters, T and V, have exactly the same meaning as the corresponding generic parameters for TryBase[T <: Throwable, +V]. This trait represents the results of a failed computation.

Here’s version 0.5 of the trait FailureBase[T <: Throwable, +V] (achieves goals 1-5):


AG1t3 - Code Snippet #4: v0.5 trait FailureBase[T <: Throwable, +V]


trait FailureBase[T <: Throwable, +V] extends TryBase[T, V] {

  val isSuccess = //explicit override to val per parent specification

    false

  val isFailure = //explicit override to val per parent specification

    !isSuccess

  def get =

    throw t

  def getOrElse[W >: V](default: => W): W =

    default

  def toOption: Option[V] =

    None

  def filter(p: V => Boolean): TryBase[T, V] =

    this

  def flatMap[W](f: V => TryBase[T, W]): TryBase[T, W] =

    this.asInstanceOf[TryBase[T, W]]

  def flatten[W](implicit ev: V <:< TryBase[T, W]): TryBase[T, W] =

    this.asInstanceOf[TryBase[T, W]]

  def map[W](f: V => W): TryBase[T, W] =

    this.asInstanceOf[TryBase[T, W]]

  def transform[W](fv: V => TryBase[T, W], ft: T => TryBase[T, W]): TryBase[T, W] =

    ft(t)

  def orElse[W >: V](default: => TryBase[T, W]): TryBase[T, W] =

    default

  def recover[W >: V](f: PartialFunction[T, W]): TryBase[T, W] =

    if (f.isDefinedAt(t))

      tryObjectBase(f(t))

    else

      this.asInstanceOf[TryBase[T, W]]

  def recoverWith[W >: V](f: PartialFunction[T, TryBase[T, W]]): TryBase[T, W] =

    if (f.isDefinedAt(t))

      f(t)

    else

      this.asInstanceOf[TryBase[T, W]]

  def foreach[W](f: V => W): Unit =

    ()

}


Defining TryRuntimeException

We now have all the parts necessary to compose a concrete implementation, abstract class TryRuntimeException[+V], with its corresponding two concrete classes; SuccessRuntimeException[+V] and FailureRuntimeException[+V] and the companion object for TryRuntimeException[+V]. Per our design goal, the apply method for the concrete companion object will only filter to instances of Throwable at the level of RuntimeException and its descendants. If a Throwable instances passes the filter, it is placed into an instance of the FailureRuntimeException[+V] case class. Otherwise, the Throwable instance remains unhandled and free to flow back up the execution chain.

Here’s version 0.5 of the TryRuntimeException(x2), SuccessRuntimeException and FailureRuntimeException (achieves goals 1-3):


AG1t3 - Code Snippet #5: v0.5 entity set TryRuntimeException[+V] 


object TryRuntimeException extends TryObjectBase[RuntimeException] {

  def successT[V]: V => TryRuntimeException[V] =

    SuccessRuntimeException(_)

  def failureT[V]: RuntimeException => TryRuntimeException[V] =

    FailureRuntimeException(_)

}

sealed abstract class TryRuntimeException[+V]

  extends TryBase[RuntimeException, V] {

    protected val tryObjectBase =

      TryRuntimeException

  }

final case class SuccessRuntimeException[+V](override val v: V)

  extends TryRuntimeException[V] with SuccessBase[RuntimeException, V]

final case class FailureRuntimeException[+V](override val t: RuntimeException)

  extends TryRuntimeException[V] with FailureBase[RuntimeException, V]


Given this new TryRuntimeException implementation, the original code using Try involving so much boilerplate for filtering RuntimeException (shown with logging omitted here as we aren’t satisfying goal 5 yet)…


AG1t3 - Code Snippet #6: Try boilerplate with logging omitted


import scala.util._

//...

def evaluateTheRiskyThing: Int =

  3/0

  //throw new Throwable("uhoh")

//...

val tryEvaluateTheRiskyThing1 =

  Try(evaluateTheRiskyThing).recoverWith {

    case runtimeException: RuntimeException =>

      Failure(runtimeException) //an additional instantiation

    case throwable: Throwable =>

      //Surprise!!! The rethrown throwable below gets

      //  re-caught by this Try method and

      //  turned into a Failure anyway! ARGH!

      throw throwable

  }

//...

val tryEvaluateTheRiskyThing2 =

  Try(evaluateTheRiskyThing) match {

    case failure @ Failure(throwable) =>

      throwable match {

        case runtimeException: RuntimeException =>

          failure

        case _ =>

          throw throwable //this rethrow actually works

      }

    case success @ _ =>

      success

  }


...now looks this simple (sans emitting the logging as we haven’t satisfied goal 5 yet):


AG1t3 - Code Snippet #7: TryRunException[Int] with logging omitted


import org.public_domain.trys.v1.TryRuntimeException

def evaluateTheRiskyThing: Int =

  3/0

  //throw new Throwable("uhoh")

//...

val tryRuntimeExceptionEvaluateTheRiskyThing1 =

  TryRuntimeException(evaluateTheRiskyThing)

val tryRuntimeExceptionEvaluateTheRiskyThing2 =

  TryRuntimeException(evaluateTheRiskyThing)


Defining TryThrowable and TryException

To finish goal 3, I have provided the two other implementations; TryThrowable and TryException below.

Here’s version 0.5 of the TryThrowable (x2), SuccessThrowable and FailureThrowable (achieves goals 1-3):


AG1t3 - Code Snippet #8: 0.5 of entity set TryThrowable[+V]


object TryThrowable extends TryObjectBase[Throwable] {

  def successT[V]: V => TryThrowable[V] =

    SuccessThrowable(_)

  def failureT[V]: Throwable => TryThrowable[V] =

    FailureThrowable(_)

}

sealed abstract class TryThrowable[+V]

  extends TryBase[Throwable, V] {

    protected val tryObjectBase =

      TryThrowable

  }

final case class SuccessThrowable[+V](override val v: V)

  extends TryThrowable[V] with SuccessBase[Throwable, V]

final case class FailureThrowable[+V](override val t: Throwable)

  extends TryThrowable[V] with FailureBase[Throwable, V]


Here’s version 0.5 of the TryException (x2), SuccessException and FailureException (achieves goals 1-3):


AG1t3 - Code Snippet #9: v0.5 of entity set TryException[+V] 


object TryException extends TryObjectBase[Exception] {

  def successT[V]: V => TryException[V] =

    SuccessException(_)

  def failureT[V]: Exception => TryException[V] =

    FailureException(_)

}

sealed abstract class TryException[+V]

  extends TryBase[Exception, V] {

    protected val tryObjectBase =

      TryException

  }

final case class SuccessException[+V](override val v: V)

  extends TryException[V] with SuccessBase[Exception, V]

final case class FailureException[+V](override val t: Exception)

  extends TryException[V] with FailureBase[Exception, V]


Using these two new implementations is just as easy as using TryRuntimeException. And given the *Base templates, it should be trivial to derive a custom one of your own whenever you need or like.


Achieving Goal 4 - Creating a Drop-in Replacement for Try

Accommodating a precise contractual behavioral replica of Try is achievable, but requires quite a few non-trivial “adjustments” and overrides to TryThrowable. The new entity set is called TryThrowableNonFatal and precisely works as a drop in replacement for Try. The references to NonFatal are to scala.util.control.NonFatal to ensure the behavior is identical between Try and this drop-in replacement.

Here’s version 0.5 of the TryThrowableNonFatal (x2), SuccessThrowableNonFatal and FailureThrowableNonFatal (achieves goals 1-4) showing as a delta from TryThrowable:


AG4 - Code Snippet #1: v0.5 entity set TryThrowableNonFatal[+V] shown as delta from TryThrowable[+V]


object TryThrowableNonFatal extends TryObjectBase[Throwable] {

  def successT[V]: V => TryThrowableNonFatal[V] =

    SuccessThrowableNonFatal(_)

  def failureT[V]: Throwable => TryThrowableNonFatal[V] =

    FailureThrowableNonFatal(_)

  override def isInstanceValid(t: Throwable): Boolean =

    NonFatal(t)

}

sealed abstract class TryThrowableNonFatal[+V]

  extends TryBase[Throwable, V] {

    protected val tryObjectBase =

      TryThrowableNonFatal

    def failed: TryBase[Throwable, Throwable]

  }

final case class SuccessThrowableNonFatal[+V](override val v: V)

  extends TryThrowableNonFatal[V] with SuccessBase[Throwable, V] {

    override def filter(p: V => Boolean): TryBase[Throwable, V] =

      try

        super.filter(p)

      catch {

        case NonFatal(e) =>

          tryObjectBase.failureT(e).asInstanceOf[TryBase[Throwable, V]]

      }

    override def flatMap[W](f: V => TryBase[Throwable, W]): TryBase[Throwable, W] =

      try

        super.flatMap(f)

      catch {

        case NonFatal(e) =>

          tryObjectBase.failureT(e).asInstanceOf[TryBase[Throwable, W]]

      }

    override def transform[W](

      fv: V => TryBase[Throwable, W],

      ft: Throwable => TryBase[Throwable, W]

    ): TryBase[Throwable, W] =

      try

        fv(v)

      catch {

        case NonFatal(e) =>

          tryObjectBase.failureT(e).asInstanceOf[TryBase[Throwable, W]]

      }

    def failed =

      tryObjectBase.failureT[Throwable](

        new UnsupportedOperationException("Success.failed")

      )

  }

final case class FailureThrowableNonFatal[+V](override val t: Throwable)

  extends TryThrowableNonFatal[V] with FailureBase[Throwable, V] {

    override def transform[W](

      fv: V => TryBase[Throwable, W],

      ft: Throwable => TryBase[Throwable, W]

    ): TryBase[Throwable, W] =

      try

        ft(t)

      catch {

        case NonFatal(e) =>

          tryObjectBase.failureT(e).asInstanceOf[TryBase[Throwable, W]]

      }

    override def orElse[W >: V](default: => TryBase[Throwable, W]): TryBase[Throwable, W] =

      try

        default

      catch {

        case NonFatal(e) =>

          tryObjectBase.failureT(e).asInstanceOf[TryBase[Throwable, W]]

      }

    override def recover[W >: V](f: PartialFunction[Throwable, W]): TryBase[Throwable, W] =

      try

        super.recover(f)

      catch {

        case NonFatal(e) =>

          tryObjectBase.failureT(e).asInstanceOf[TryBase[Throwable, W]]

      }

    override def recoverWith[W >: V](

      f: PartialFunction[Throwable, TryBase[Throwable, W]]

    ): TryBase[Throwable, W] =

      try

        super.recoverWith(f)

      catch {

        case NonFatal(e) =>

          tryObjectBase.failureT(e).asInstanceOf[TryBase[Throwable, W]]

      }

    val failed =

      tryObjectBase.successT(t)

  }


For me, the presence of the extra try/catch wrappers is a code smell. And it also appears to be inconsistent with the core principles behind DbC; i.e. the methods do not behave consistently with their explicit specification. I am sure there is valid reasoning to support this pattern. And obviously the pattern works as it has been used extensively across thousands of projects in production. And that is why I decided it was worth providing a backwards compatible implementation.

At the very least, it makes it easier for those who would like to start a transitioning to this newer model but already have a codebase full of Try. And the strongest reason they might want to begin the transition is the subject of the next section; substantially reducing the amount of boilerplate for emitting logging on Throwable pathway, captured into a FailureBase or not.


Achieving Goal 5 - Reducing Throwable Pathway Logging Boilerplate

All of what follows starts with the assumption an instance of Throwable has been thrown. There are two possible pathways:

  1. The Throwable is a valid narrowing to capture and place into a FailureBase
  2. The Throwable does not vall within the valid narrowing and must remain uncaptured, either by being ignored or by being explicitly re-thrown

In each case, I want to provide the ability for the client to be able to provide individually customized logging per call to apply on TryObjectBase. Further, it is important to allow the client to specify the customized logging once and then have any further calls to apply on TryObjectBase use them. And there is nothing better to optimally perform this job than Scala’s implicit mechanism.

So, first we must refactor the apply method on TryObjectBase to accommodate these new requirements. We will both be specifying the interface (method implicit parameters) as well as the implementation (the body of the method using the new implicit parameters).

Here’s version 0.6 of the trait TryObjectBase[T <: Throwable] (achieves goals 1-5):


AG5 - Code Snippet #1: v0.6 trait TryObjectBase[T <: Throwable] with logging hooks added


import scala.reflect.ClassTag

//unable to make a trait - won't compile with ": ClassTag"

abstract class TryObjectBase[T <: Throwable : ClassTag] {

  def successT[V]: V => TryBase[T, V]

  def failureT[V]: T => TryBase[T, V]

  protected def isInstanceValid(t: Throwable): Boolean =

    true

  def apply[V]

    (vToEvaluate: => V)

    (implicit

      failureTEvent: FailureBase[T, V] => Unit =

        (_: Any) => (), //due to a compiler bug, unable to use the simpler "_ => ()"

      optionThrowableEvent: Option[(Throwable => Boolean, Throwable => Unit)] =

        None

    )

  : TryBase[T, V] =

    optionThrowableEvent match {

      case Some((isThrowableEventInstanceValid, throwableEvent)) =>

        try

          successT(vToEvaluate)

        catch {

          case t: T if isInstanceValid(t) =>

            val failureTTemp =

              failureT[V](t.asInstanceOf[T])

            failureTEvent(failureTTemp.asInstanceOf[FailureBase[T, V]])

            failureTTemp

          case throwable: Throwable if isThrowableEventInstanceValid(throwable) =>

            throwableEvent(throwable)

            throw throwable

        }

      case None =>

        try

          successT(vToEvaluate)

        catch {

          case t: T if isInstanceValid(t) =>

            val failureTTemp =

              failureT[V](t.asInstanceOf[T])

            failureTEvent(failureTTemp.asInstanceOf[FailureBase[T, V]])

            failureTTemp

        }

    }

}


The expansion of apply seems to be code redundant. This is an example of where the Scala/FP world and the Java/JVM/OO world diverge specifically regarding performance concerns. It is also a wonderful thing that Scala embraces these kinds of pragmatic issues exist and offer so many options for elegant AND performant implementations hidden behind well designed interfaces.

It turns out that it is more execution performant to specialize each try/catch based on the definition of its sequence of catch clauses. In this case, the Option is being used to ensure that if the client isn’t requesting to be informed of the Throwable pathway, the catch clause never catches it in the first place; i.e. it entirely skips the catch and rethrow cycle which is the precise point of the poor performance in this JVM area.

Given an updated TryRuntimeException implementation using this new version of TryObjectBase[T <: Throwable], the original code using Try which involved so much boilerplate for filtering RuntimeException now, using the _explicit_ mechanism, looks like this (still plenty of boilerplate):


AG5 - Code Snippet #2: TryRuntimeException logging boilerplate


def evaluateTheRiskyThing =

  3/0

  //throw new Throwable("uhoh")

//...

TryRuntimeException(evaluateTheRiskyThing) (

  failureBase =>

    println(s"Failure: ${Option(failureBase.t.getMessage).getOrElse("<null>")}"),

  Some(

    (

      (_: Throwable) =>

        true,

      (throwable: Throwable) =>

        println(s"Uncaught: ${Option(throwable.getMessage).getOrElse("<null>")}")

    )

  )

)


Ah, but the magic of Scala’s implicits save the day. Here’s what it looks like when using Scala’s implicit mechanism:


AG5 - Code Snippet #3: using implicits to reduce logging boilerplate for  TryRuntimeException 


implicit val failureTEventFunc =

  (failureBase: FailureBase[RuntimeException, _]) =>

    println(s"Failure: ${Option(failureBase.t.getMessage).getOrElse("<null>")}")

implicit val optionThrowableEventFunc =

  Some(

    (

      (_: Throwable) =>

        true,

      (throwable: Throwable) =>

        println(s"Uncaught: ${Option(throwable.getMessage).getOrElse("<null>")}")

    )

  )

//...

def evaluateTheRiskyThing =

  3/0

def evaluateTheOtherRiskyThing =

  4/0

def evaluateTheAnotherRiskyThing =

  5/0

//...

TryRuntimeException(evaluateTheRiskyThing)

TryRuntimeException(evaluateTheOtherRiskyThing)

TryRuntimeException(evaluateTheAnotherRiskyThing)


By defining the two implicits first (necessary boilerplate), and then later calling apply on TryRuntimeException, there isn’t a need to explicitly supply the parameters to the apply method. The Scala compiler figures out that’s what is meant and then automatically handles it for you.

And just in case you were thinking you’d rather not even provide the (boilerplate for the) implicits, you don’t have to. If you look closely at the definition of the two implicits in the apply methods parameters, you will see they both have been provided default values (essentially No-OPs). I find Scala’s mechanisms for facilitating this kind of reduction in code boilerplate both pleasurable and fantastic.


Achieving Goal 6 - Optionally Capturing Enclosing Context for FailureBase

There are lots of possible implementation choices for achieving goal 6, optionally capturing enclosing context for FailureBase. I have chosen an implementation which works optimally for the use cases I am personally hitting in my Geospatial Scala/Akka based SaaS project, BricksAndMortar.

To facilitate easier debugging and possibly even further enhance logging, being able to capture some level of local context at the time of the generation of the FailureBase is valuable. For example, one of the cases I hit frequently is mapping over a List[_] and getting back a List[Try[_]], then scanning the List[Try[_]] to see if there were any instances of Failure, and if so, collecting those and then creating a new summarising Failure which contains the concatenated error messages from the collected Failures. I would rather just pass the Failures and perhaps some other information per Failure (ex: the ordinal index in the original List) to a new Failure and then defer to the client optionally handling the mapping of the information into something the client finds useful.

In this general spirit, an enclosingContext method is added to FailureBase. It’s type will be List[(FailureBase[_ <: Throwable, _], Option[java.io.Serializable])] representing a List of associations between a FailureBase and an optional instance of something implementing the java.io.Serializable interface.

Sidenote: The java.ioSerializable interface was chosen (as opposed to Any) as it is probable that a FailureBase instance would need to be pushed across a process boundary. When that happens, most marshalling mechanisms (ex: Akka) need the content to be java.io.Serializable to be able to transmit it from one actor in a JVM process to another actor in a separate JVM process possibly executing on entirely different hardware. This information is ignored by this implementation and intended for client use only.

Here’s version 1.0 of the trait FailureBase[T <: Throwable, +V] (achieves goals 1-6):


AG6 - Code Snippet #1: v1.0 trait FailureBase[T <: Throwable, +V] with enclosingContext added


trait FailureBase[T <: Throwable, +V] extends TryBase[T, V] {

  def enclosingContext:

    List[(FailureBase[_ <: Throwable, _], Option[java.io.Serializable])]

  val isSuccess = //explicit override to val per parent specification

    false

  val isFailure = //explicit override to val per parent specification

    !isSuccess

  def get =

    throw t

  def getOrElse[W >: V](default: => W): W =

    default

  def toOption: Option[V] =

    None

  def filter(p: V => Boolean): TryBase[T, V] =

    this

  def flatMap[W](f: V => TryBase[T, W]): TryBase[T, W] =

    this.asInstanceOf[TryBase[T, W]]

  def flatten[W](implicit ev: V <:< TryBase[T, W]): TryBase[T, W] =

    this.asInstanceOf[TryBase[T, W]]

  def map[W](f: V => W): TryBase[T, W] =

    this.asInstanceOf[TryBase[T, W]]

  def transform[W](fv: V => TryBase[T, W], ft: T => TryBase[T, W]): TryBase[T, W] =

    ft(t)

  def orElse[W >: V](default: => TryBase[T, W]): TryBase[T, W] =

    default

  def recover[W >: V](f: PartialFunction[T, W]): TryBase[T, W] =

    if (f.isDefinedAt(t))

      tryObjectBase(f(t))

    else

      this.asInstanceOf[TryBase[T, W]]

  def recoverWith[W >: V](f: PartialFunction[T, TryBase[T, W]]): TryBase[T, W] =

    if (f.isDefinedAt(t))

      f(t)

    else

      this.asInstanceOf[TryBase[T, W]]

  def foreach[W](f: V => W): Unit =

    ()

}


So, now I must adjust the TryObjectBase to accommodate this new method which is exclusively present on FailureBase (and not on TryBase as SuccessBase does not have a design need for it).

Here’s version 1.0 of the trait TryObjectBase[T <: Throwable] (achieves goals 1-6):


AG6 - Code Snippet #2: v1.0 abstract class TryObjectBase[T <: Throwable] with enclosingContext added


import scala.reflect.ClassTag

//unable to make a trait - won't compile with ": ClassTag"

abstract class TryObjectBase[T <: Throwable : ClassTag] {

  def successT[V]: V => TryBase[T, V]

  def failureT[V]:

    (

      T,

      List[(FailureBase[_ <: Throwable, _], Option[java.io.Serializable])]

    ) => TryBase[T, V]

  protected def isInstanceValid(t: Throwable): Boolean =

    true

  def apply[V](

    vToEvaluate: => V,

    failureEnclosingContext:

      List[

        (

          FailureBase[_ <: Throwable, _],

          Option[java.io.Serializable]

        )

      ] =

      Nil

  ) (

    implicit failureTEvent: FailureBase[T, V] => Unit =

      (_: Any) => (), //due to a compiler bug, unable to use the simpler "_ => ()"

    optionThrowableEvent: Option[(Throwable => Boolean, Throwable => Unit)] =

      None

  ): TryBase[T, V] =

    optionThrowableEvent match {

      case Some((isThrowableEventInstanceValid, throwableEvent)) =>

        try

          successT(vToEvaluate)

        catch {

          case t: T if isInstanceValid(t) =>

            val failureTTemp =

              failureT[V](t.asInstanceOf[T], failureEnclosingContext)

            failureTEvent(failureTTemp.asInstanceOf[FailureBase[T, V]])

            failureTTemp

          case throwable: Throwable if isThrowableEventInstanceValid(throwable) =>

            throwableEvent(throwable)

            throw throwable

        }

      case None =>

        try

          successT(vToEvaluate)

        catch {

          case t: T if isInstanceValid(t) =>

            val failureTTemp =

              failureT[V](t.asInstanceOf[T], failureEnclosingContext)

            failureTEvent(failureTTemp.asInstanceOf[FailureBase[T, V]])

            failureTTemp

        }

    }

}


Now SuccessBase must be touched to fix a call to TryObjectBase.


AG6 - Code Snippet #3: v1.0 trait SuccessBase[T <: Throwable, +V]


trait SuccessBase[T <: Throwable, +V] extends TryBase[T, V] {

  val isSuccess = //explicit override to val per parent specification

    true

  val isFailure = //explicit override to val per parent specification

    !isSuccess

  def get =

    v

  def getOrElse[W >: V](default: => W): W =

    get

  def toOption: Option[V] =

    Some(get)

  def filter(p: V => Boolean): TryBase[T, V] =

    if (p(v))

      this

    else

      tryObjectBase.failureT[V](

        new IllegalArgumentException(

          s"filter predicate does not hold for v [$v]"

        ).asInstanceOf[T],

        Nil

      )

  def flatMap[W](f: V => TryBase[T, W]): TryBase[T, W] =

    f(v)

  def flatten[W](implicit ev: V <:< TryBase[T, W]): TryBase[T, W] =

    this.asInstanceOf[TryBase[T, W]]

  def map[W](f: V => W): TryBase[T, W] =

    tryObjectBase(f(v))

  def transform[W](fv: V => TryBase[T, W], ft: T => TryBase[T, W]): TryBase[T, W] =

    fv(v)

  def orElse[W >: V](default: => TryBase[T, W]): TryBase[T, W] =

    this.asInstanceOf[TryBase[T, W]]

  def recover[W >: V](f: PartialFunction[T, W]): TryBase[T, W] =

    this.asInstanceOf[TryBase[T, W]]

  def recoverWith[W >: V](f: PartialFunction[T, TryBase[T, W]]): TryBase[T, W] =

    this.asInstanceOf[TryBase[T, W]]

  def foreach[W](f: V => W): Unit =

    f(v)

}


That concludes the changes on the *Base classes. To finish goal 6, each of the concrete implementations must also be touched in order to ensure the FailureBase derivation properly initializes the new enclosingContext method (overriding and then changing it to a val) we added to FailureBase.

Here’s version 1.0 of the TryThrowable (x2), SuccessThrowable and FailureThrowable (achieves goals 1-6):


AG6 - Code Snippet #4: v1.0 trait TryThrowable[+V] with enclosingContext added


object TryThrowable extends TryObjectBase[Throwable] {

  def successT[V]: V => TryThrowable[V] =

    SuccessThrowable(_)

  def failureT[V]:

    (

     Throwable,

     List[(FailureBase[_ <: Throwable, _], Option[java.io.Serializable])]

    ) => TryThrowable[V] =

    (t, failureEnclosingContext) =>

      FailureThrowable(t, failureEnclosingContext)

}

sealed abstract class TryThrowable[+V]

  extends TryBase[Throwable, V] {

    protected val tryObjectBase =

      TryThrowable

  }

final case class SuccessThrowable[+V](override val v: V)

  extends TryThrowable[V] with SuccessBase[Throwable, V]

final case class FailureThrowable[+V](

  override val t: Throwable,

  override val enclosingContext:

    List[(FailureBase[_ <: Throwable, _], Option[java.io.Serializable])] =

    Nil

)

  extends TryThrowable[V] with FailureBase[Throwable, V]


Here’s version 1.0 of the TryException (x2), SuccessException and FailureException (achieves goals 1-6):


AG6 - Code Snippet #5: v1.0 trait TryException[+V] with enclosingContext added


object TryException extends TryObjectBase[Exception] {

  def successT[V]: V => TryException[V] =

    SuccessException(_)

  def failureT[V]:

    (

      Exception,

      List[(FailureBase[_ <: Throwable, _], Option[java.io.Serializable])]

    ) => TryException[V] =

    (t, failureEnclosingContext) =>

      FailureException(t, failureEnclosingContext)

}

sealed abstract class TryException[+V]

  extends TryBase[Exception, V] {

    protected val tryObjectBase =

      TryException

  }

final case class SuccessException[+V](override val v: V)

  extends TryException[V] with SuccessBase[Exception, V]

final case class FailureException[+V](

  override val t: Exception,

  override val enclosingContext:

    List[(FailureBase[_ <: Throwable, _], Option[java.io.Serializable])] =

    Nil

)

  extends TryException[V] with FailureBase[Exception, V]


Here’s version 1.0 of the TryRuntimeException (x2), SuccessRuntimeException and FailureRuntimeException (achieves goals 1-6):


AG6 - Code Snippet #6: v1.0 trait TryRuntimeException[+V] with enclosingContext added


object TryRuntimeException extends TryObjectBase[RuntimeException] {

  def successT[V]: V => TryRuntimeException[V] =

    SuccessRuntimeException(_)

  def failureT[V]:

    (

      RuntimeException,

      List[(FailureBase[_ <: Throwable, _], Option[java.io.Serializable])]

    ) => TryRuntimeException[V] =

    (t, failureEnclosingContext) =>

      FailureRuntimeException(t, failureEnclosingContext)

}

sealed abstract class TryRuntimeException[+V]

  extends TryBase[RuntimeException, V] {

    protected val tryObjectBase =

      TryRuntimeException

  }

final case class SuccessRuntimeException[+V](override val v: V)

  extends TryRuntimeException[V] with SuccessBase[RuntimeException, V]

final case class FailureRuntimeException[+V](

  override val t: RuntimeException,

  override val enclosingContext:

    List[(FailureBase[_ <: Throwable, _], Option[java.io.Serializable])] =

    Nil

)

  extends TryRuntimeException[V] with FailureBase[RuntimeException, V]


Here’s version 1.0 of the TryThrowableNonFatal (x2), SuccessThrowableNonFatal and FailureThrowableNonFatal (achieves goals 1-6):


AG6 - Code Snippet #7: v1.0 trait TryThrowableNonFatal[+V] with enclosingContext added


object TryThrowableNonFatal extends TryObjectBase[Throwable] {

  def successT[V]: V => TryThrowableNonFatal[V] =

    SuccessThrowableNonFatal(_)

  def failureT[V]:

    (

      Throwable,

      List[(FailureBase[_ <: Throwable, _], Option[java.io.Serializable])]

    ) => TryThrowableNonFatal[V] =

    (t, failureEnclosingContext) =>

      FailureThrowableNonFatal(t, failureEnclosingContext)

  override def isInstanceValid(t: Throwable): Boolean =

    super.isInstanceValid(t) && NonFatal(t)

}

sealed abstract class TryThrowableNonFatal[+V]

  extends TryBase[Throwable, V] {

    protected val tryObjectBase =

      TryThrowableNonFatal

    def failed: TryBase[Throwable, Throwable]

  }

final case class SuccessThrowableNonFatal[+V](override val v: V)

  extends TryThrowableNonFatal[V] with SuccessBase[Throwable, V] {

    override def filter(p: V => Boolean): TryBase[Throwable, V] =

      try

         super.filter(p)

      catch {

        case NonFatal(e) =>

          tryObjectBase.failureT(e, Nil).asInstanceOf[TryBase[Throwable, V]]

      }

    override def flatMap[W](f: V => TryBase[Throwable, W]): TryBase[Throwable, W] =

      try

         super.flatMap(f)

      catch {

        case NonFatal(e) =>

          tryObjectBase.failureT(e, Nil).asInstanceOf[TryBase[Throwable, W]]

      }

    override def transform[W](

      fv: V => TryBase[Throwable, W],

      ft: Throwable => TryBase[Throwable, W]

    ): TryBase[Throwable, W] =

      try

         fv(v)

      catch {

        case NonFatal(e) =>

          tryObjectBase.failureT(e, Nil).asInstanceOf[TryBase[Throwable, W]]

      }

    def failed =

      tryObjectBase.failureT[Throwable](

        new UnsupportedOperationException("Success.failed"),

        Nil

       )

  }

final case class FailureThrowableNonFatal[+V](

  override val t: Throwable,

  override val enclosingContext:

    List[(FailureBase[_ <: Throwable, _], Option[java.io.Serializable])] =

    Nil

)

  extends TryThrowableNonFatal[V] with FailureBase[Throwable, V] {

    override def transform[W](

      fv: V => TryBase[Throwable, W],

      ft: Throwable => TryBase[Throwable, W]

    ): TryBase[Throwable, W] =

      try

         ft(t)

      catch {

        case NonFatal(e) =>

          tryObjectBase.failureT(e, enclosingContext).asInstanceOf[TryBase[Throwable, W]]

      }

    override def orElse[W >: V](default: => TryBase[Throwable, W]): TryBase[Throwable, W] =

      try

         default

      catch {

        case NonFatal(e) =>

          tryObjectBase.failureT(e, enclosingContext).asInstanceOf[TryBase[Throwable, W]]

      }

    override def recover[W >: V](f: PartialFunction[Throwable, W]): TryBase[Throwable, W] =

      try

         super.recover(f)

      catch {

        case NonFatal(e) =>

          tryObjectBase.failureT(e, enclosingContext).asInstanceOf[TryBase[Throwable, W]]

      }

    override def recoverWith[W >: V](

      f: PartialFunction[Throwable, TryBase[Throwable, W]]

    ): TryBase[Throwable, W] =

      try

         super.recoverWith(f)

      catch {

        case NonFatal(e) =>

          tryObjectBase.failureT(e, enclosingContext).asInstanceOf[TryBase[Throwable, W]]

      }

    val failed =

      tryObjectBase.successT(t)

  }


And to demonstrate how this might be used:


AG6 - Code Snippet #8: leveraging added enclosingContext


implicit val failureTEventFunc =

  (failureBase: FailureBase[RuntimeException, _]) =>

    println(s"Failure: ${Option(failureBase.t.getMessage).getOrElse("<null>")}")

implicit val optionThrowableEventFunc =

  Some(

    (

      (_: Throwable) =>

        true,

      (throwable: Throwable) =>

        println(s"Uncaught: ${Option(throwable.getMessage).getOrElse("<null>")}")

    )

  )

val tryBase: List[TryBase[_ <: Throwable, _]] =

  List(

    TryRuntimeException(1 + 1), //(failureTEventFunc, optionThrowableEventFunc)

    TryRuntimeException(3/0),   //(failureTEventFunc, optionThrowableEventFunc)

    TryException(2/0),          //(parameter defaults)

    TryThrowable(1/0),          //(parameter defaults)

    TryThrowableNonFatal(0/0)   //(parameter defaults)

  )

val tryBasesFailureBasesOnly =

  tryBase.zipWithIndex.filter(_._1.isFailure)

val failureBases: List[(FailureBase[_ <: Throwable, _], Option[java.io.Serializable])] =

  tryBasesFailureBasesOnly.map {

    case (a, b) =>

      val bs: Option[java.io.Serializable] =

        Some(b) //necessary to do here as the commented out implicit conversions below never happen

      (

        a.asInstanceOf[FailureBase[_ <: Throwable, _]],

        bs

        //Some(b) //doesn't compile - the implicit conversion never happens

        //Some[Serializable](b) //doesn't compile - the implicit conversion never happens

        //Some(int2Integer(b)) //turning the implicit conversion explicit

      )

  }

val failureRuntimeException =

  FailureRuntimeException(new IllegalStateException(), failureBases)



Conclusion

The completed versions of all these classes have been added to my open source project, ScalaOlio (see http://scalaolio.org). The base traits have been moved into a package named “org.scalaolio.util.trys.templates” with the concrete implementations in the package “org.scalaolio.util.trys”.

Please email me (jim dot oflaherty dot jr at qalocate dot com) with any questions, issues or concerns.

####

1 comment: