validations in value classes

2019-02-15 15:30发布

问题:

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