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)
NonNegativeInt(-100)to returnNonNegativeInt(0). Your case class name itself is very clear that it is non-negative soapply()seems good to me. Are you saying you want to call something asSomeClass.someMethodThatReturnsNonNegative(int: Int): NonoNegativeInt