3

I'm thinking of making a value class that has some guard on how it can be instantiated. For the sake of example, say I want a non-negative integer:

class NonNegInt private (val value: Int) extends AnyVal

object NonNegInt {
  def apply(value: Int): Try[NonNegInt] = Try {
    if (value >= 0) new NonNegInt(value) else throw new IllegalArgumentException("non-negative integers only!")
  }
}

My only worry is that the private constructor may make it impossible for the scala compiler to treat the NonNegInt as a primitive int. Is this true?

2rs2ts
  • 10,662
  • 10
  • 51
  • 95
  • Did you try to compile this code? – Dima Dec 30 '14 at 19:53
  • @Dima Yes, it compiles if you import `scala.util.Try`. I didn't bother to include it in my example because it's implicit that you'd import it. No need for extra clutter. – 2rs2ts Dec 30 '14 at 19:56
  • I know. I mean, it either works or it does not. It seems easier to just compile and run a sample and see if it does what you want than to bother asking this question on SO, doesn't it? – Dima Dec 30 '14 at 19:58
  • 1
    @Dima You misunderstand the question. I'm asking if a private constructor on a value class makes it impossible for the compiler to treat it as a primitive. See [When Allocation Is Necessary](http://docs.scala-lang.org/overviews/core/value-classes.html#when-allocation-is-necessary) and [Limitations](http://docs.scala-lang.org/overviews/core/value-classes.html#limitations). – 2rs2ts Dec 30 '14 at 20:00
  • Well, if you are worried about allocation ... `NonNegInt.apply` returns an instance of `Try`, which, of course, needs to be allocated. Does that answer your question? – Dima Dec 30 '14 at 20:05
  • @Dima No, it doesn't answer my question, because if it's getting treated as a `Try[Int]` because `NonNegInt` is supposed to be a value class then it's not a problem. – 2rs2ts Dec 30 '14 at 20:07
  • No, it is definitely treated as `Try[NonNegInt]` – Dima Dec 30 '14 at 20:10
  • @Dima Can you explain why this is in an answer so I can accept it? – 2rs2ts Dec 30 '14 at 20:12
  • 1
    Well, for example, consider this excerpt from the guide you linked to: `Another instance of this rule is when a value class is used as a type argument. For example, the actual Meter instance must be created for even a call to identity`. Basically, because `identity[T]` is parametrized, invoking it on a value type requires an allocation of an instance. `Try[T]` is the same situation. – Dima Dec 30 '14 at 20:19
  • @Dima I didn't make any type parameterized methods. Are you saying that the `Try { }` block is basically me using it as a type parameter? – 2rs2ts Dec 30 '14 at 20:35
  • Yes, `Try { ... }` is a call to a parametrized function `Try.apply[NonNegInt]` – Dima Dec 30 '14 at 21:04
  • @Dima can you write as much in an answer so I can accept it and give you some well-earned reputation? :) – 2rs2ts Dec 30 '14 at 21:16

3 Answers3

3

If "treat as a primitive" here means "avoid allocation", then this indeed will not work, but not because of a private constructor.

As mentioned in Value Classes Guide

Another instance of this rule is when a value class is used as a type argument. For example, the actual Meter instance must be created for even a call to identity.

def identity[T](t: T): T = t
identity(Meter(5.0))

Basically, because identity[T] is parametrized, invoking it on a value type requires an allocation of an instance. Try[T] is the same situation: Try { ... } "block" is an invocation of a parametrized function Try.apply[T] with T being NonNegInt. This call will require an allocation of NonNegInt instance.

Onema
  • 7,331
  • 12
  • 66
  • 102
Dima
  • 39,570
  • 6
  • 44
  • 70
1

This is a hint:

scala> implicit class X private (val i: Int) extends AnyVal { def doubled = 2 * i }
<console>:7: error: constructor X in class X cannot be accessed in object $iw
       implicit class X private (val i: Int) extends AnyVal { def doubled = 2 * i }
                      ^

And this is definitive:

$ scala -optimise
Welcome to Scala version 2.11.4 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_11).
Type in expressions to have them evaluated.
Type :help for more information.

scala> :pa
// Entering paste mode (ctrl-D to finish)

class X private (val i: Int) extends AnyVal { def doubled = 2 * i }
object X { @inline def apply(i: Int) = new X(i) }

// Exiting paste mode, now interpreting.

defined class X
defined object X

scala> X(42).doubled
warning: there was one inliner warning; re-run with -Yinline-warnings for details
res0: Int = 84

You can use :javap -prv - to verify that there was an allocation.

But this is a better trick:

scala> case class X private (val i: Int) extends AnyVal { def doubled = 2 * i }
defined class X

scala> X(42).doubled
res1: Int = 84

scala> :javap -prv -
[snip]
  public $line7.$read$$iw$$iw$();
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0       
         1: invokespecial #19                 // Method java/lang/Object."<init>":()V
         4: aload_0       
         5: putstatic     #21                 // Field MODULE$:L$line7/$read$$iw$$iw$;
         8: aload_0       
         9: getstatic     #26                 // Field $line6/$read$$iw$$iw$X$.MODULE$:L$line6/$read$$iw$$iw$X$;
        12: bipush        42
        14: invokevirtual #30                 // Method $line6/$read$$iw$$iw$X$.doubled$extension:(I)I
        17: putfield      #17                 // Field res1:I
        20: return  

Footnote:

scala> case class X[A <: X[A]] private (val i: Int) extends AnyVal { def doubled = 2 * i }
defined class X

scala> X(42).doubled
res2: Int = 84
som-snytt
  • 39,429
  • 2
  • 47
  • 129
  • Some of this is beyond me. Are you saying that unless I add that `[A <: X[A]]` it's going to allocate? I don't even understand what that means :) – 2rs2ts Dec 30 '14 at 21:54
  • Me neither. case class loosens the access restriction, so the private ctor isn't a deal breaker. The other answer asks about what if it appears as a type arg. The F-bounded example may not add anything, since it just takes Nothing for A. – som-snytt Dec 31 '14 at 02:40
  • My understanding was that value classes weren't allowed to define `equals` and `hashCode` which is something a case class does. So doesn't that defeat the purpose? – 2rs2ts Dec 31 '14 at 18:20
  • 1
    Only non-synthetics are disallowed; case class equals/hash is the same as defined for value class. – som-snytt Jan 01 '15 at 01:16
  • "non-synthetics" meaning explicitly defined ones? – 2rs2ts Jan 06 '15 at 18:57
  • @2rs2ts Yes. "synthetics" are added by the compiler. – som-snytt Jan 06 '15 at 20:04
0

Your example code is making your actual question ambiguous. Your example code wraps the Int in a Try. If instead of using Try, you used a require statement in the companion object, then it's my understanding the code below would work (without losing the "primitive" benefits extending AnyVal offers). This would give you a runtime exception if/when there is an attempt to produce a negative value. The code uses a private constructor on the case class extending AnyVal. Then it uses the case class's companion object's apply method to enforce runtime constraints via a require statement.

If you really need to wrap the value using a Try, you can provide an additional companion object constructor to wrap apply to capture the exception. However, as is pointed out in other answers, you lose the AnyVal "primitive" quality when it is "contained" by a Try, Option, Either, etc.

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)
  }

  def tryApply(value: Int): Try[PositiveInt] =
    Try(apply(value))
}
case class PositiveInt private(value: Int) extends AnyVal

val positiveTestA = PositiveInt(0)
val positiveTestB = PositiveInt(1)
val positiveTestD = PositiveInt.tryApply(-1)) //returns Failure
val positiveTestD = Try(PositiveInt(-1))      //returns Failure
val positiveTestC = PositiveInt(-1)           //throws required exception
chaotic3quilibrium
  • 5,661
  • 8
  • 53
  • 86