1

I'd like to use case classes to describe the types of my data more expressively so that to benefit from higher static correctness. The goal is to have 100% static certainty that any Age value in existence always contains a valid human age (leaving aside the fact that encapsulation rules can be bypassed using reflection).

For example, instead of using Int to store ages of persons, I have:

case class Age(x: Int) extends AnyVal
def mkAge(x: Int) = if (0 <= x && x <= 150) Some(Age(x)) else None
def unwrapAge(age: Age) = age.x

however, this implementation suffers from the fact that Age can still be instantiated without going through mkAge and unwrapAge.

Next, I tried to make the constructor private:

case class Age private(x: Int) extends AnyVal
object Age {
  def make(x: Int) = if (0 <= x && x <= 150) Some(Age(x)) else None
  def unwrap(age: Age) = age.x
}

however, while this does prevent Age from being instantiated using new (e.g. new Age(3)), the autogenerated apply(x: Int) in object Age is still easily accessible.

So, here's the question: how to hide both the constructor as well as the default apply method in the companion object from anything but Age.make or mkAge?

I'd like to avoid having to use a regular (non-case) class and correctly replicate the auto-generated methods in class Age and object Age manually.

Erik Kaplun
  • 37,128
  • 15
  • 99
  • 111

3 Answers3

1

You were almost there:

case class Age private(private val x:Int) extends AnyVal
object Age {
  def mkAge(x:Int) = if(0<=x && x<=150) Some(Age(x)) else None
  def unwrapAge(age:Age) = age.x
}

Note the extra private val inside the case class constructor.

Madoc
  • 5,841
  • 4
  • 25
  • 38
  • This doesn't compile in 2.10.3 at least: "error: value class needs to have a publicly accessible val parameter". And even if it did, it wouldn't help. – Alexey Romanov Nov 12 '14 at 06:19
  • @AlexeyRomanov Sorry, then it's not possible with 2.10. I compiled it with 2.11. It does reach your goal: Neither the constructor nor the companion object's apply method can be called from the outside, and x is also not publicly accessible. In which way doesn't it help? – Madoc Nov 12 '14 at 07:12
  • "nor the companion object's apply method can be called from the outside" Then this is a Scala bug, since `Age.apply` visibility shouldn't depend on `x`'s. According to http://www.scala-lang.org/files/archive/spec/2.11/05-classes-and-objects.html#case-classes, it's always public. – Alexey Romanov Nov 12 '14 at 08:47
  • `x` is not accessible, but this isn't asked in the question. – Alexey Romanov Nov 12 '14 at 08:49
1

So, here's the question: how to hide both the constructor as well as the default apply method in the companion object from anything but Age.make or mkAge?

I'd like to avoid having to use a regular (non-case) class and correctly replicate the auto-generated methods in class Age and object Age manually.

I thought it was impossible, but https://stackoverflow.com/a/25538287/9204 details a (rather non-trivial) solution.

Community
  • 1
  • 1
Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487
  • There are some problems with that solution: A) It's very complex, as you mentioned. B) It produces a warning, as extending case classes is discouraged and might be forbidden in a future Scala version. C) You won't get the case class benefits without rewriting them yourself, which was a requirement. D) I'm not sure if it's even possible to have an abstract value class, and even when, the instances will be likely to be boxed most of the time anyway. – Madoc Nov 12 '14 at 07:18
  • @Madoc C) What benefits do you lose? All methods are still generated and inherited by the anonymous class. The only one you have to rewrite is `copy` and I don't think you can work around that. D) Yes, you lose value class; if it's a requirement, this won't work. – Alexey Romanov Nov 12 '14 at 08:58
0

I think Age just don't need to be a case class. Because it is a Value Class you don't need to override equals and hashcode, also it has only one field so there is no benefit from copy constructor. And you can do nothing with apply() in companion object. If you still want to use case class you can add require but it doesn't solve you problem of instantiation.

object A extends App {

  import Age._

  println(mkAge(150))
  println(mkAge(151))
  //println(new Age(51))   //Error!


  val a = mkAge(15) match {
    case Some(Age(x)) => x
    case None => 0
  }

  print(a)
}

class Age private(val x: Int) extends AnyVal {
  override def toString = s"A($x)"
}

object Age {
  def mkAge(x: Int) = if (0 <= x && x <= 150) Some(new Age(x)) else None
  def unwrapAge(age: Age) = age.x
  def unapply(age: Age) = if (age == null) None else Some(age.x)
}
ka4eli
  • 5,294
  • 3
  • 23
  • 43
  • if it's a value type, it does need equals and hashcode; also, the fact of it having just one field is arbitrary — it might well have 10 fields. – Erik Kaplun Nov 13 '14 at 12:31
  • It will no longer be a Value Class with more than 1 field. So for Value Classes to be also a case class isn't an advantage, I think. Is being a Value Class important? – ka4eli Nov 13 '14 at 16:41
  • I didn't say AnyVal — value types are a general term. AnyVal is just an optimisation. – Erik Kaplun Nov 13 '14 at 20:47