Scala Snacks: Part 2 - Don't assert your requirement

This one will actually be a snack, I promise.

Let’s paint a picture. We’ve got a DTO containing some state. Let’s make it about football this time, a Goals class.

case class Goals(value: Int)  

The shape of the class is merely for demonstrative purposes, I won't be mentioning anything about value classes.

It jumps out that we should probably have some validation around this, after all we cannot have -1 goals. Easy enough:

case class Goals(value: Int) {  
  assert(value >= 0, s”$value is not a valid number of goals")
}

This will throw at runtime, which isn’t ideal, but if our code is adequately tested that should suffice, shouldn't it?

An assert in Scala throws a java.lang.AssertionError. A require, on the other hand, throws a java.lang.IllegalArgumentException. There’s a subtle semantic difference here. This stackoverflow response puts it nicely.

  • assert means that your program has reached an inconsistent state - this might be a problem with the current method/function
  • require means that the caller of the method is at fault and should fix its call

More generally we can consider the differences between an Error and an Exception, both of which are Throwable. Our application may, and probably should, treat errors and exceptions differently. Certainly our situation is more akin to rejecting the caller who provided this illegal state by throwing an exception. Throwing an error here is analogous to a giant spider attempting to enter your house, and instead of closing the door on the spider you decide to burn the place to the ground with you and the spider in it. A bit overdramatic, and not in the same ball park as a VirtualMachineError, for example. However if your house was infested with killer rats, it may be reasonable to consider that an unrecoverable state - light it up and take the insurance money... Alright enough weird analogies.

Ok fair enough, use require. As long as all throwable are treated equally it's only a semantic difference, so who really cares? Turns out there’s a reason to care at least a bit. I came across a problem which stemmed from this slight misdemeanour.

A user noticed a frontend error with the message java.util.concurrent.ExecutionException: Boxed Error. Not too descriptive, right? This is how errors thrown inside Scala futures are wrapped. The underlying error is obscured but can be pulled out using getCause - see here for more details. On the REPL:

scala> val errorF: Future[Int] = Future.apply(throw new AssertionError("Bad things"));  
errorF: scala.concurrent.Future[Int] = Future(<not completed>)

scala> errorF.onComplete { case Failure(e) => println(s"Throwable message is: ${e.getMessage}"); println(s"Throwable cause is: ${e.getCause}"); case _ => }  
Throwable message is: Boxed Error  
Throwable cause is: java.lang.AssertionError: Bad things  

The justification for this in the Scala docs is related to the aforementioned semantic differences between errors and exceptions. All throwables aren’t created equally, and with good reason.

After a little digging through a couple of services I got to the underlying error, and changed an assert to a require. I thought this was vaguely interesting so I’d share it, and maybe save someone else a bit of time.

Totally off topic

As an aside, an interesting (albeit impractical) way to guarantee a positive number of goals at compile time could be to use natural numbers. Then we can use a type alias or in future define Goals as an opaque type. Certainly there are libraries which do this in a neater and more efficient way, but if we were to implement from scratch we might have something like:

sealed trait NaturalNumber { def toInt: Int }  
case object Zero extends NaturalNumber { def toInt = 0 }  
case class Successor(predecessor: NaturalNumber) extends NaturalNumber {  
  def toInt = 1 + predecessor.toInt
}

It would be nice to be able to sum and multiply them. Obviously we can't use the integer form - that would be cheating.

object NaturalNumber {  
  def sum(n: NaturalNumber, m: NaturalNumber): NaturalNumber = (n, m) match {
    case (Zero, _) => m
    case (Successor(x), _) => Successor(sum(x, m))
  }

  def multiply(n: NaturalNumber, m: NaturalNumber): NaturalNumber = (n, m) match {
    case (Zero, _) => Zero
    case (Successor(Zero), _) => m
    case (Successor(x), _) => sum(m, multiply(x, m)) // sum 1 lot of m and n-1 lots of m
  }
}

Having a play on the REPL:

scala> val one = Successor(Zero)  
one: Successor = Successor(Zero)

scala> val two = Successor(one)  
two: Successor = Successor(Successor(Zero))

scala> val three = sum(one, two)  
three: NaturalNumber = Successor(Successor(Successor(Zero)))

scala> val six = multiply(two, three)  
six: NaturalNumber = Successor(Successor(Successor(Successor(Successor(Successor(Zero))))))

scala> val fortyOne  = sum(multiply(six, six), sum(two, three)).toInt  
fortyOne: Int = 41  

I'll leave def power(n: NaturalNumber, m: NaturalNumber): NaturalNumber as a bit of fun for the reader - but be warned your stack will grow quickly since everything boils down to recursive calls to sum.

That concludes this Scala snack. If you're still hungry, check out some of our other blogs:

Photo Credit