Algebraic types that can be instantiated via a sentinel function only

92 views Asked by At

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.

3

There are 3 answers

3
ka4eli On

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)
}
4
Madoc On

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.

2
Alexey Romanov On

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.