validations in value classes

拈花ヽ惹草 提交于 2019-12-01 00:53:01

You could use refined to lift the validation step to compile time by refining your Double with refined's Positive predicate:

import eu.timepit.refined.auto._
import eu.timepit.refined.numeric._
import shapeless.tag.@@

scala> implicit class Volatility(val underlying: Double @@ Positive) extends AnyVal
defined class Volatility

scala> Volatility(1.5)
res1: Volatility = Volatility@3ff80000

scala> Volatility(-1.5)
<console>:52: error: Predicate failed: (-1.5 > 0).
       Volatility(-1.5)
                   ^

Note that the last error is a compile error and not a runtime error.

An implicit conversion to a type marked as having passed your runtime requirement.

scala> trait Pos
defined trait Pos

scala> implicit class P(val i: Int with Pos) extends AnyVal { def f = i }
defined class P

scala> implicit def cv(i: Int): Int with Pos = { require(i>0); i.asInstanceOf[Int with Pos] }
warning: there was one feature warning; re-run with -feature for details
cv: (i: Int)Int with Pos

scala> new P(42).f
res0: Int with Pos = 42

scala> :javap -prv -
        17: invokevirtual #35                 // Method $line5/$read$$iw$$iw$.cv:(I)I
        20: invokevirtual #38                 // Method $line4/$read$$iw$$iw$P$.f$extension:(I)I

scala> new P(-42).f
java.lang.IllegalArgumentException: requirement failed
  at scala.Predef$.require(Predef.scala:207)
  at .cv(<console>:13)
  ... 33 elided

You can also have private methods that enforce invariants.

scala> implicit class P(val i: Int with Pos) extends AnyVal { private def g = require(i>0) ; def f = { g; i } }
defined class P

scala> new P(-42.asInstanceOf[Int with Pos]).f
java.lang.IllegalArgumentException: requirement failed
  at scala.Predef$.require(Predef.scala:207)
  at P$.$line10$$read$P$$g$extension(<console>:14)
  at P$.f$extension(<console>)
  ... 33 elided

The way I have accomplished this is to use the companion object's .apply method to add a require constraint prior to calling the case class's private constructor "instantiating" the value.

WARNING: The code below will not compile in the REPL/Scala Worksheet. A case class extending AnyVal must be a top-level class; i.e. cannot be nested within the scope of another class, trait, or object. And both the REPL and Scala Worksheet are implemented by pushing all the code into an invisible containing class before executing.

object PositiveInt {
  def apply(value: Int): PositiveInt = {
    require(value >= 0, s"value [$value] must be greater than or equal to 0")
    new PositiveInt(value)
  }
}
case class PositiveInt private(value: Int) extends AnyVal

val positiveTestA = PositiveInt(0)
val positiveTestB = PositiveInt(1)
val positiveTestC = PositiveInt(-1) //throws required exception
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!