wdb wdb - 11 months ago 43
Scala Question

How can I enforce compile-time constraints on values for Scala methods?

I want to enforce constraints the parameter values for Scala methods at compile-time.

For example:

case class Foo(numberOfFoo: Int, ...)

is an
above, but I'd really like to make it a positive integer. I have tried classes such as PositiveInt to enforce this, but that just pushes the check to another class where still it is not compile-time checked.

Using the example above, I want this:

val n: Int = ...
val f: Foo = Foo(n)

to compile if
n > 0
and not compile if
n <= 0
. I don't want the instantiating code to have to handle a possible exception, process an
, or end up with a
Foo.numberOfFoo != n
(i.e. I don't want to use the absolute value of the input parameter).

UPDATE: Thanks for the helpful information. It is as I feared. Mostly, I wanted to be able specify the size of something that must have a positive integral size. So this seems to be the best approach:

case class Foo(bar: Bar) {val n = bar size}

Answer Source

Another way of doing this is by using the shapeless library, and using Nat. The limitation is that you will need to instantiate those Foo entities at compile time basically, with known constants.

import shapeless.ops.nat_
import shapeless.nat._

case class Foo[T <: Nat](numberOfFoo: Int)(implicit ev: GT[T, _0]

object Foo {
  // The problem is this won't work.
  def apply[T <: Nat](n: Int): Foo[T] = Foo(Nat(n))

This will work only if used like this:


Where _1 comes from shapeless.nat._. If you drill down into the implementation, the 0 part happens to be enforced even without you meaning to by shapeless:

 if (n < 0) c.abort(c.enclosingPosition, s"A Nat cannot represent $n")

Stick to simpler things

This is however quite cumbersome, because whichever approach you take, it will rely on a macro, and macros cannot really work unless the value is known at compile time. This can become very limiting, if the simplest delegation method no longer works.

In practice, it may be more efficient to go around this kind of approach and use the normal ways. Whether you use shapeless or the refined library mentioned above, the story doesn't change, so for a normal use case it's probably cleaner to do runtime validation.