huoenter
huoenter

Reputation: 522

Overwriting the default case class constructor (Scala)

If I have a case class:

case class NonNegativeInt(i: Int)

where the field i to be set to 0 if the parameter is negative. So I cannot just use the default constructor provided by the case class. If I define an apply() method in the companion object (or wherever):

def apply(n: Int) = new NonNegativeInt(Math.max(0, n))

Apparently they have the same signature. Is there a practical way/pattern to process the constraints on the fields?

Upvotes: 0

Views: 2001

Answers (3)

muradm
muradm

Reputation: 2053

There is no straight forward way to override case class constructor as far as I know. However, assuming that real data type won't be simple int, you could do some type which considers invalid state like below:

sealed abstract class NonNegativeInt { def isValid: Boolean }
final case class ValidNonNegativeInt(i: Int) extends NonNegativeInt { override def isValid: Boolean = true }
final case object InvalidNonNegativeInt extends NonNegativeInt { override def isValid: Boolean = false }
object NonNegativeInt {
  def apply(i: Int): NonNegativeInt = if (i < 0) InvalidNonNegativeInt else ValidNonNegativeInt(i)
}

This works pretty simple:

scala>   NonNegativeInt(0)
res5: NonNegativeInt = ValidNonNegativeInt(0)

scala>   NonNegativeInt(-1)
res6: NonNegativeInt = InvalidNonNegativeInt

Then you can even do pattern matching:

val ni = NonNegativeInt(10)
ni match {
    case ValidNonNegativeInt(i) => println(s"valid $i")
    case InvalidNonNegativeInt => println(s"invalid")
}

Then you could further extend your functionality with map/flatMap etc.

Of course it still not protecting you from negative case:

scala>   ValidNonNegativeInt(-10)
res7: ValidNonNegativeInt = ValidNonNegativeInt(-10)

But scala Option for instance also does not override constructor for Some() case allowing invalid value:

scala> Option(null)
res8: Option[Null] = None

scala> Some(null)
res9: Some[Null] = Some(null)

Unless there is no critical use case, for simple Int I would leave it as it is, and ensure its correctness in usages. For more complex structures, above way is quite useful.

Note: I intentionally not used your Max(0, n) way, as in this case it will cause more problems than it would solve. Assuming something, and swapping data under the hood is bad practice. Imagine you will have a bug somewhere in other place of your code which will use your implementation with Max(0, n). If input data would be -10, most likely, problem was caused by some other problem in incoming data. When you change it to default 0, even through input was -10, later when you will analyze logs, dumps or debug output, you will miss the fact that it was -10.


Other solutions, in my point of view:

@flavian solution is most logical. Explicit functionality/validation

@Cyrille Corpet: very Java'ish

@jwvh solution will take double amount of memory footprint, since it will be two Ints in memory. And also will not protect from overriding:

scala>   case class NonNegativeInt1(private val x:Int)(implicit val i:Int = Math.max(0,x)) {
     |     override def toString: String = s"NonNegativeInt1($x, $i)"
     |   }
defined class NonNegativeInt1

scala>   NonNegativeInt1(5)
res10: NonNegativeInt1 = NonNegativeInt1(5, 5)

scala>   NonNegativeInt1(-5)
res11: NonNegativeInt1 = NonNegativeInt1(-5, 0)

scala>   NonNegativeInt1(-5)(-5)
res12: NonNegativeInt1 = NonNegativeInt1(-5, -5)

Upvotes: -1

Cyrille Corpet
Cyrille Corpet

Reputation: 5305

Although I mostly agree with @flavian's answer that you should use another name for your method, what you can do is not make a case class at all. Or rather, implement by hand all that the case class construct gives you:

class NonNegativeInt private (val i: Int) {
  override def equals(that: Any): Boolean = that.isInstanceOf[NonNegativeInt] && that.asInstanceOf[NonNegativeInt].i == i
  override def hashCode = i.hashCode

  def copy(i: Int = this.i) = NonNegativeInt(i)  //use companion apply method, not private constructor
}

object NonNegativeInt {
  def apply(i: Int) = 
    new NonNegativeInt(if (i < 0) 0 else i)

  def unapply(that: NonNegativeInt): Option[Int] = Some(that.i)
}

Upvotes: 2

flavian
flavian

Reputation: 28511

case class NonNegativeInt(i: Int)

If you can't use apply just name it something else.

object NonNegativeInt {
  def fromInt(i: Int): NonNegativeInt = NonNegativeInt(Math.max(0, i)
}

You can get fancier if you like, typecheck literal constants with compile time restrictions on positive ints with Refined or shapeless, hide the primary constructor by sealing the case class or other such means, but it feels a bit overkill under the circumstances.

Upvotes: 3

Related Questions