0

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?

1
  • 1
    I don't understand why you would not want NonNegativeInt(-100) to return NonNegativeInt(0). Your case class name itself is very clear that it is non-negative so apply() seems good to me. Are you saying you want to call something as SomeClass.someMethodThatReturnsNonNegative(int: Int): NonoNegativeInt Commented Jun 8, 2017 at 20:55

3 Answers 3

3
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.

Sign up to request clarification or add additional context in comments.

Comments

2

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

Comments

-1

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)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.