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?
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.