Search code examples
scalaassertprimitive

How do I prevent invalid values in an AnyVal derivative


In an attempt to remain strongly typed, prevent invalid states, and maintain the efficiencies of a JVM primitive type, I am attempting to do the following which is returning a compilation error of "this statement is not allowed in value class - assert(!((double < -180.0d) || ...".

case class Longitude(double: Double) extends AnyVal {
  assert(!((double < -180.0d) || (double > 180.0d)), s"double [$double] must not be less than -180.d or greater than 180.0d")

  def from(double: Double): Option[Longitude] =
    if ((double < -180.0d) || (double > 180.0d))
      None
    else
      Some(Longitude(double))
}

My desired effect is to prevent invalid instances from existing, like Longitude(-200.0d). What options do I have for achieving the desired effect?


Solution

  • There is an amazing library Refined which aimed to solved exactly this sort of problems: prove on type level certain validation. Also this approach know in community as "Making illegal states unrepresentable". More then then - it provides compilation level checks along with runtime validations.

    In your case possible solution might look like:

    import eu.timepit.refined._
    import eu.timepit.refined.api.Refined
    import eu.timepit.refined.auto._
    import eu.timepit.refined.numeric._
    import eu.timepit.refined.boolean._
    
    type LongtitudeValidation = Greater[W.`180.0`.T] Or Less[W.`-180.0`.T]
    
    /**
    * Type alise for double which should match condition `((double < -180.0d) || (double > 180.0d))` at type level
    */
    type Longtitude = Double Refined LongtitudeValidation
    
    val validLongTitude: Longtitude = refineMV(190.0d))
    
    val invalidLongTitude: Longtitude = refineMV(160.0d)) //this won't compile because of validation failures
    //error you will see: Both predicates of ((160.0 > 180.0) || (160.0 < -180.0)) failed. Left: Predicate failed: (160.0 > 180.0). Right: Predicate failed: (160.0 < -180.0).
    

    Also you can use runtime verification via refineV method:

    type LongtitudeValidation = Greater[W.`180.0`.T] Or Less[W.`-180.0`.T]
    type Longtitude = Double Refined LongtitudeValidation
    
    val validatedLongitude1: Either[String, Longtitude] = refineV(190.0d)
    println(validatedLongitude1)
    
    val validatedLongitude2: Either[String, Longtitude] = refineV(160.0d)
    println(validatedLongitude2)
    

    which will print out:

    Right(190.0)
    Left(Both predicates of ((160.0 > 180.0) || (160.0 < -180.0)) failed. Left: Predicate failed: (160.0 > 180.0). Right: Predicate failed: (160.0 < -180.0).)
    

    You can play and check by yourself in Scatie: https://scastie.scala-lang.org/CQktleObQlKWKYby0vaszA

    UPD:

    Thanks to @LuisMiguelMejíaSuárez who suggested to use refined with scala-newtype to avoid additional memory allocations.