3

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
Sachin K
  • 195
  • 1
  • 8

4 Answers4

3

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
som-snytt
  • 39,429
  • 2
  • 47
  • 129
  • 1
    I edited my original question to clarify that the validation shouldn't ideally be a separate method call. – Sachin K Oct 16 '15 at 13:26
3

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.

Frank S. Thomas
  • 4,725
  • 2
  • 28
  • 47
  • Many thanks, this does answer my question prefectly. Does it preserve the advantage of AnyVal wrapper around primitive that no primitive boxing instance gets created at runtime? – Sachin K Oct 19 '15 at 00:00
  • 1
    Unfortunately not. Using `@@` here will cause the `Double` to be boxed (see https://github.com/fthomas/refined#performance-concerns for more details). But I'm currently exploring ways how boxing could be avoided for primitive types (https://github.com/fthomas/refined/issues/76). – Frank S. Thomas Oct 19 '15 at 19:20
0

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
chaotic3quilibrium
  • 5,661
  • 8
  • 53
  • 86
0

Would this work for your use case? Make the constructor private and use the companion object with validation logic to create new instances.

class User private (val userIdentifier:String) extends AnyVal {}

object User {
  def apply(userIdentifier: String): User = {
    if(Option(userIdentifier).exists(_.trim.isEmpty)) throw new IllegalArgumentException("User identifier cannot be empty!")
    new User(userIdentifier)
  }
}
Viswanath
  • 1,413
  • 13
  • 25