1

Summary:
I want to have options for validation prior to instantiating a Scala case class as opposed to having to use the requires/IllegalArgumentException mechanism. Is there a way to avoid Java-like boilerplate code when attempting to pre-validate parameters intended for a Scala case class instantiation?

Details:
Coming from Java, I've now used Scala case classes enough to thoroughly enjoy and appreciate how much boilerplate it eliminates. However, I am now hitting a problem which seems to be boilerplate ballooning my Scala code around case classes considerably.

Please consider the following code for my typical Scala case class (copied directly from IntelliJ's Scala Worksheet - NOTE: Everything above the case class Surface1 is there to reduce code noise when presenting Surface2):

val LONGITUDE_MAX = 180.0d
val LATITUDE_MAX = 90.0d
def isLongitudeValid(longitude: Double) = (-LONGITUDE_MAX <= longitude) && (longitude <= LONGITUDE_MAX)
def isLatitudeValid(latitude: Double) = (-LATITUDE_MAX <= latitude) && (latitude <= LATITUDE_MAX)
object SurfaceType extends Enumeration {
  type SurfaceType = Value
  val SEA, LAND, ICE = Value
}
def isSurfaceTypeCorrect(longitude: Double, latitude: Double, surfaceType: SurfaceType.Value) = true //fabricated validation assuming longitude and latitude are validated
//
//#1. Simple case class use
case class Surface1(longitude: Double = 0.0d, latitude: Double = 0.0d, surfaceType: SurfaceType.Value = SurfaceType.SEA) {
  require(isLongitudeValid(longitude), s"longitude [$longitude] must be greater than or equal to -$LONGITUDE_MAX and less than or equal to $LONGITUDE_MAX")
  require(isLatitudeValid(latitude), s"latitude [$latitude] must be greater than or equal to -$LATITUDE_MAX and less than or equal to $LATITUDE_MAX")
  require(isSurfaceTypeCorrect(longitude, latitude, surfaceType), s"for longitude[$longitude] and latitude[$latitude], surfaceType [$surfaceType] must be the correct")
}

So, given the above for Surface1, here are some example usages:

val surface1a = Surface1()
val surface1b = Surface1(0.0d, 0.0d, SurfaceType.SEA)
val surface1c = Surface1(-180.0d, -90.0d, SurfaceType.ICE)
val surface1d = Surface1(-180.1d, -90.1d, SurfaceType.ICE) //generates an IllegalArgumentException

The first three will generate a correct instance. The last will generate an IllegalArgumentException...BUT ONLY FOR LONGITUDE. Latitude is also erred. But with the standard Scala case class model, the validation for latitude an surfaceType are won't be evaluated. It would be very helpful for at least latitude's validation to have executed also and received a list of exceptions.

So, after numerous tangents, this is what I arrived at to be able to pre-instantiation validation where I can do any of; continue to instantiate with the standard requires/exception model (apply), return an Option (get) or return Either (create):

//#2. Explicit validators for case class use
type CaseClassValidationException = IllegalArgumentException
object Surface2 {
  def validate(longitude: Double, latitude: Double, surfaceType: SurfaceType.Value): Option[List[CaseClassValidationException]] = {
    val errorsA =
      List(
          if (!isLongitudeValid(longitude)) Some(new CaseClassValidationException(s"longitude [$longitude] must be greater than or equal to -$LONGITUDE_MAX and less than or equal to $LONGITUDE_MAX")) else None
        , if (!isLatitudeValid(latitude)) Some(new CaseClassValidationException(s"latitude [$latitude] must be greater than or equal to -$LATITUDE_MAX and less than or equal to $LATITUDE_MAX")) else None
      )
    val errorsB =
      if (errorsA.isEmpty) //these checks depend upon the errorsA checks
        List(
            if (!isSurfaceTypeCorrect(longitude, latitude, surfaceType))
              Some(new CaseClassValidationException(s"for longitude[$longitude] and latitude[$latitude], surfaceType [$surfaceType] must be the correct"))
            else None
        )
      else Nil
    val errors = (errorsA ::: errorsB).flatten
    if (!errors.isEmpty) Some(errors)
    else None
  }
  def get(longitude: Double = 0.0d, latitude: Double = 0.0d, surfaceType: SurfaceType.Value = SurfaceType.SEA): Option[Surface2] = {
    create(longitude, latitude, surfaceType) match {
      case Right(surface2) => Some(surface2)
      case Left(errors) => None
    }
  }
  def create(longitude: Double = 0.0d, latitude: Double = 0.0d, surfaceType: SurfaceType.Value = SurfaceType.SEA): Either[List[CaseClassValidationException], Surface2] = {
    validate(longitude, latitude, surfaceType) match {
      case Some(errors) => Left(errors)
      case None => Right(new Surface2(longitude, latitude, surfaceType, false))
    }
  }
}
case class Surface2 private (longitude: Double, latitude: Double, surfaceType: SurfaceType.Value, executeValidate: Boolean) {
  def this(longitude: Double = 0.0d, latitude: Double = 0.0d, surfaceType: SurfaceType.Value = SurfaceType.SEA) = this(longitude, latitude, surfaceType, true)
  if (executeValidate) require(Surface2.validate(longitude, latitude, surfaceType).isEmpty, "failed validate")
}

So, given the above for Surface2, here are some example usages:

val surface2a = new Surface2()
val surface2b = new Surface2(0.0d, 0.0d, SurfaceType.SEA)
val surface2c = new Surface2(-180.0d, -90.0d, SurfaceType.ICE)
val surface2d = new Surface2(-180.1d, -90.1d, SurfaceType.ICE) //generates an IllegalArgumentException
val surface2option1a = Surface2.get()
val surface2option1b = Surface2.get(0.0d, 0.0d, SurfaceType.SEA)
val surface2option1c = Surface2.get(-180.0d, -90.0d, SurfaceType.ICE)
val surface2option1d = Surface2.get(-180.1d, -90.1d, SurfaceType.ICE) //None
val surface2either1a = Surface2.create()
val surface2either1b = Surface2.create(0.0d, 0.0d, SurfaceType.SEA)
val surface2either1c = Surface2.create(-180.0d, -90.0d, SurfaceType.ICE)
val surface2either1d = Surface2.create(-180.1d, -90.1d, SurfaceType.ICE) //Left[...]

The first four produce the same results as Surface1 does. However, the remaining 8 will only instantiate when the validate method returns None. So, I have achieved my desired effects, but at a code smell cost. Here are the issues I have with the solution:

  1. Inefficient instantiation space usage with the extra executeValidate parameter
  2. Exposed implementation detail - executeValidate parameter
  3. Much larger code surface (i.e. boilerplate) increasing the probability of introducing errors/bugs

I have dozens of case classes I will be using in my current project. And it sure feels awfully heavy to have to add this to each case class as I define it. Surely I am overlooking something where I can substantially simplify producing the desired effects without the listed undesired effects.

Any guidance you can offer would be greatly appreciated.

UPDATE 2014-06-13:
It appears (without using Scalaz) there is no real means to significantly reduce the boilerplate. That said, thanks to NikitaVolkov and his aligning with my preference to avoid using Exceptions as much as possible, I realized that I don't need the "apply" method in the case class itself (which is what forces the extra inefficient executeValidate parameter for the private case class constructor). This was a significant simplification of the case class itself; i.e. removing the exception throwing apply method entirely.

Below is the latest version, Surface3, which achieves all my desired effects and while shorter and simpler, is still pretty long on the boilerplate side:

object SurfaceType extends Enumeration {
  type SurfaceType = Value
  val SEA, LAND, ICE = Value
}
type CaseClassValidationException = IllegalArgumentException
object Surface3 {
  def longitudeValidate(longitude: Double): Option[CaseClassValidationException] = {
    val longitudeBound = 180.0d
    if (!((-longitudeBound <= longitude) && (longitude <= longitudeBound)))
      Some(new CaseClassValidationException(s"longitude [$longitude] must be greater than or equal to -$longitudeBound and less than or equal to $longitudeBound"))
    else None
  }
  def latitudeValidate(latitude: Double): Option[CaseClassValidationException] = {
    val latitudeBound = 90.0d
    if (!((-latitudeBound <= latitude) && (latitude <= latitudeBound)))
      Some(new CaseClassValidationException(s"latitude [$latitude] must be greater than or equal to -$latitudeBound and less than or equal to $latitudeBound"))
    else None
  }
  def surfaceTypeValidate(longitude: Double, latitude: Double, surfaceType: SurfaceType.Value): Option[CaseClassValidationException] = None //fabricated validation assuming longitude and latitude are validated
  private def fullValidate(longitude: Double, latitude: Double, surfaceType: SurfaceType.Value): Option[List[CaseClassValidationException]] = {
    val errors1 =
      List(
          longitudeValidate(longitude)
        , latitudeValidate(latitude)
      )
    val errors2 =
      if (errors1.isEmpty)
        List(
           surfaceTypeValidate(longitude, latitude, surfaceType)
        )
      else Nil
    val errorsFinal = (errors1 ::: errors2).flatten
    if (errorsFinal.nonEmpty) Some(errorsFinal)
    else None
  }
  def createEither(longitude: Double = 0.0d, latitude: Double = 0.0d, surfaceType: SurfaceType.Value = SurfaceType.SEA): Either[List[CaseClassValidationException], Surface3] =
    fullValidate(longitude, latitude, surfaceType) match {
      case Some(errors) => Left(errors)
      case None => Right(new Surface3(longitude, latitude, surfaceType))
    }
  def createOption(longitude: Double = 0.0d, latitude: Double = 0.0d, surfaceType: SurfaceType.Value = SurfaceType.SEA): Option[Surface3] =
    createEither(longitude, latitude, surfaceType) match {
      case Right(surface3) => Some(surface3)
      case Left(_) => None
    }
}
case class Surface3 private (longitude: Double, latitude: Double, surfaceType: SurfaceType.Value)
//
val surface3option1a = Surface3.createOption()
val surface3option1b = Surface3.createOption(0.0d, 0.0d, SurfaceType.SEA)
val surface3option1c = Surface3.createOption(-180.0d, -90.0d, SurfaceType.ICE)
val surface3option1d = Surface3.createOption(-180.1d, -90.1d, SurfaceType.ICE) //None
val surface3either1a = Surface3.createEither()
val surface3either1b = Surface3.createEither(0.0d, 0.0d, SurfaceType.SEA)
val surface3either1c = Surface3.createEither(-180.0d, -90.0d, SurfaceType.ICE)
val surface3either1d = Surface3.createEither(-180.1d, -90.1d, SurfaceType.ICE) //Left[...]
chaotic3quilibrium
  • 5,661
  • 8
  • 53
  • 86
  • If you don't mind having an additionnal dependency, you should look at ValidationNel from scalaz. Take a look a this [post](http://stackoverflow.com/questions/12307965/method-parameters-validation-in-scala-with-for-comprehension-and-monads/12309023#12309023) for further infos. – LMeyer Jun 12 '14 at 19:40
  • For now, I'd rather figure out the minimal boilerplate solution without Scalaz. Tysvm for the suggestion, though. – chaotic3quilibrium Jun 13 '14 at 18:42

2 Answers2

0

Never express logic with exceptions. That might be a standard practice in Java, but nonetheless it is an antipattern. The same applies to require, since it's just a wrapper around a throw.

Validation is a perfectly logical operation, whose results can easily be expressed with any of the following standard types: Boolean, Option, Either. The last one you can use to pass some specific info about the validation failure, like a String with a description message. What's more important the types Option and Either allow you to encode the validity of data by wrapping it.

There are some other problems in your code. You introduce constants and couple your code by redundantly depending on them in multiple places. Another thing is that case classes are really supposed to be just data without any extra fuss like what you introduced.

Here's how your problem can be approached:

case class Surface1(longitude: Double = 0.0d, latitude: Double = 0.0d, surfaceType: SurfaceType.Value = SurfaceType.SEA)

type ValidSurface1 = Either[String, Surface1]
object ValidSurface1 {
  def validateLongitude(longitude: Double) = {
    val max = 180
    if( longitude.abs =< max ) Right(longitude) 
    else Left(s"longitude [$longitude] must have an absolute greater than or equal to $max")
  }
  def validateLatitude(latitude: Double): Either[String, Double] = 
    sys.error("TODO: same as validateLongitude")
  def validateSurfaceType(longitude: Double, latitude: Double, surfaceType: SurfaceType.Value): Either[String, Double] = 
    sys.error("TODO: validation assuming longitude and latitude are validated")

  def apply(longitude: Double = 0.0d, latitude: Double = 0.0d, surfaceType: SurfaceType.Value = SurfaceType.SEA) = 
    for {
      _ <- validateLongitude(longitude)
      _ <- validateLatitude(latitude)
      _ <- validateSurfaceType(longitude, latitude, surfaceType)
    }
    yield Surface1(longitude, latitude, surfaceType)
}

You can now construct validated surfaces by calling ValidSurface1(...).

If you don't get what's happening in the apply method, we're exposing the fact that Either is a monad, and the "for-yield" notation is meant for them. You can easily find a lot of reading material on monads in Scala on the web.

Nikita Volkov
  • 42,792
  • 11
  • 94
  • 169
  • @wingedsubmariner Oh.. Otherwise there's no better way to achieve this then with applicative functors, and, hence, Scalaz. [Here is](http://stackoverflow.com/a/12309023/485115) an answer, which provides the details. – Nikita Volkov Jun 12 '14 at 20:20
  • If I am reading this solution correctly, it is allowing the case class to be constructed with any values, valid or not, with the validation being an optional operation post-instantiation. One of my requirements is to prevent an invalid instance of the case class from ever being instantiated. IOW, if an instance of the case class exists, it can be assumed to be valid. It was my understanding this is one of the benefits of a ADT (Abstract Data Type). – chaotic3quilibrium Jun 13 '14 at 13:50
  • @chaotic3quilibrium You can make the default constructor of the case class private and provide the `apply` method from this answer to its companion object instead. This way you'll ensure that there will be no other way to instantiate the class but thru validation. That is what abstract data types are about. – Nikita Volkov Jun 13 '14 at 14:20
  • @NikitaVolkov I'm trying to follow your code. However, I'm getting stuck at the validateSurfaceType function which is returning Either[String, Double]. Is that correct? If so, I don't understand how to write the body of that function. – chaotic3quilibrium Jun 13 '14 at 14:35
  • @NikitaVolkov Even with the changes you are suggestioning, it still does not appear to collect multiple errors. – chaotic3quilibrium Jun 13 '14 at 15:09
  • @chaotic3quilibrium Yes, `validateSurfaceType` is supposed to return `Either[String, Double]`. For reference you can use my implementation of `validateLongitude`. My answer implements a fail-fast strategy, i.e., it collects only the first failure. To implement aggregation of multiple errors idiomatically (functionally), you will need applicative functors. Unfortunately, the standard Scala library does not provide them, but Scalaz does. [Here is an answer](http://stackoverflow.com/a/12309023/485115), which explains in detail how it can be used to solve a problem like yours. – Nikita Volkov Jun 13 '14 at 16:13
  • @NikitaVolkov validateSurfaceType is supposed to be about the type SurfaceType.Value, not Double. So, when that function returns Right(???), shouldn't the ??? be replaced with surfaceType (which is not assignable to a Double - at least according to the IntelliJ error message)? – chaotic3quilibrium Jun 13 '14 at 18:41
  • 1
    @chaotic3quilibrium Oh, right, it was my typo. Just use `Either[String, SurfaceType.Value]` instead then. Yes just pass the `surfaceType` value. – Nikita Volkov Jun 13 '14 at 19:37
0

Here is a solution that doesn't require changes to the companion object, or repeatedly reiterating the constructor parameters. It provides a DSL similar to that of require, but allows multiple errors to be collected. It uses exceptions, and probably is about as small as the boilerplate can get:

class ValidationException(val errors: Seq[String], message: String) extends Exception(message)

class Validator {
  var errors = Vector[String]()
  def done() = {
    if (!errors.isEmpty) {
      val message = "Multiple validation errors:\n" + errors.mkString("\n")
      throw new ValidationException(errors, message)
    }
  }
  def require(b: Boolean, s: String) = {
    if (!b)
      errors :+= s
  }
  def isEmpty = errors.isEmpty
}

trait ValidatedClass {
  def validate(v: Validator): Unit

  {
    val v = new Validator
    validate(v)
    v.done()
  }
}

case class Surface2 (longitude: Double = 0d, latitude: Double = 0d, surfaceType: SurfaceType.Value = SurfaceType.SEA) extends ValidatedClass {
  import Surface2._

  def validate(v: Validator) = {
    v.require(isLongitudeValid, s"longitude [$longitude] must be greater than or equal to -$LONGITUDE_MAX and less than or equal to $LONGITUDE_MAX")
    v.require(isLatitudeValid, s"latitude [$latitude] must be greater than or equal to -$LATITUDE_MAX and less than or equal to $LATITUDE_MAX")
    if (v.isEmpty)
      v.require(isSurfaceTypeCorrect, s"for longitude[$longitude] and latitude[$latitude], surfaceType [$surfaceType] must be the correct")
  }

  // These access the constructor parameters directly and return Boolean
  def isLongitudeValid = ???
  def isLatitudeValid = ???
  def isSurfaceTypeCorrect = ???
}

I'm a big fan of exceptions. They provide failure by default, compose without boilerplate, and don't present the temptation to do a .get to just pretend the error isn't there. Either is the nightmare of checked exceptions resurrected in Haskell. Scala's Try can offer the best of both worlds, but I would leave it to client code to wrap in Try() if needed.

wingedsubmariner
  • 13,350
  • 1
  • 27
  • 52
  • If I am accurately reading your solution, it requires I instantiate the case class and then check for validity. If so, this isn't what I am looking for. My desired effect is to prevent invalid instances of the case class from ever being instantiated in the first place. IOW, if an instance of the case class exists, it can be assumed to be valid. That was my understanding of part of the value of an ADT (Abstract Data Type). – chaotic3quilibrium Jun 13 '14 at 13:45
  • @chaotic3quilibrium No, this solution checks for validity when the case class is instantiated, and the constructor will throw an exception if it is not valid. You don't need to manually call `validate`, it will be called automatically - this is done by the block in the body of `ValidatedClass`. – wingedsubmariner Jun 13 '14 at 21:18