问题
SIP-15 implies one can use value classes to define for example new numeric classes, such as positive numbers. Is it possible to code such a constraint that the underlying > 0 in absence of constructor without having to call a separate method for validating the constraint (ie; creating a valid instance of such class is succint)?
If value classes had the notion of constructor, then that could a place to have such validations such as below, but that is not supported (ie; code below will not compile)
implicit class Volatility(val underlying: Double) extends AnyVal {
require(!underlying.isNaN && !underlying.isInfinite && underlying > 0, "volatility must be a positive finite number")
override def toString = s"Volatility($underlying)"
}
Volatility(-1.0) //should ideally fail
回答1:
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.
回答2:
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
回答3:
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
来源:https://stackoverflow.com/questions/33136558/validations-in-value-classes