Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What's the correct way to enforce constraints on case class values

Tags:

scala

Throwing exceptions at construction time is generally seen as not a very polite thing to do, but I'm not sure how to avoid that when trying to enforce constraints on case class values.

Say for example that I need to represent a range, and that both boundaries must be positive. An immediate implementation would be:

case class Range(from: Int, to: Int)

This, however, does not make sure that both from and to are positive, nor that to is greater than from.

My first instinct is to implement it as follows:

case class Range(from: Int, to: Int) {
  require(from >= 0)
  require(to >= 0)
  require(to >= from)
}

This, however, makes Range's constructor unsafe.

Is there a common pattern to keep the ease of use of case classes, enforce value constraints, and avoid throwing exceptions?

like image 714
Nicolas Rinaudo Avatar asked Jun 02 '14 20:06

Nicolas Rinaudo


2 Answers

This is pretty subjective, but I'll try.

It depends on how your class is constructed. In the case of a simple utility range class used casually in internal code, any programmer using it should probably be aware that feeding a range wrong values might lead to weird results. You can assume that sane uses of the classes really don't encounter this problem, and throwing an exception in these rare cases might not be a bad practice to signal somebody really screwed up. If it is obvious from context what values should be fed to the class or not, I don't think it is particularly in bad taste to throw exceptions if people fail to meet these expectations of sanity.

Compare this to the behaviour of NullPointerExceptions or ArithmethicExceptions which still happen within Scala sometimes. Sometimes it just isn't reasonable anymore to program defensively when faced with enough 'insanity'.

On the other hand, if your class is filled by values you have less control over, i.e. they are the direct consequence of user input, you have to simply take greater control over your construction. Create a companion object with a function that returns an Option or an Either:

 object Range {
     def createSafe(from: Int, to: Int): Option[Range] = {
         if(from >= 0 && to >= 0 && to >= from)
            Some(Range(from, to))
         else
            None
     }
 }

You can then even reuse the validation logic in the instantiation of your case class by overriding apply as the other answer suggested.

 object Range {
    def createSafe ...
    def apply(from: Int, to: Int): Range = {
        createSafe(from, to).getOrElse(throw new IllegalArgumentException(...))
    }
 }
like image 139
DCKing Avatar answered Nov 14 '22 01:11

DCKing


You can overload the apply operator in the companion object to achieve what you'd like to do:

object Range{
  def apply(from: Int, to: Int) ={
    val _from = Math.max(0, from)
    val _to = Math.max(0, to)
    if(_from < _to) new Range(_from, _to)
    else new Range(_to, _from)
  }
}

Do note that using to as a variable name can lead to some "interesting" results.

like image 6
wheaties Avatar answered Nov 14 '22 00:11

wheaties