Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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, ...)

numberOfFoo is an Int 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 Option[Foo], or end up with a Foo where 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}
like image 720
wdb Avatar asked Sep 06 '16 18:09

wdb


2 Answers

You're going to have to use the refined library. That is the only way without resorting to Nat or other type tricks. From the example in the README:

import eu.timepit.refined._
import eu.timepit.refined.api.Refined
import eu.timepit.refined.auto._
import eu.timepit.refined.numeric._

// This refines Int with the Positive predicate and checks via an
// implicit macro that the assigned value satisfies it:
scala> val i1: Int Refined Positive = 5
i1: Int Refined Positive = 5

// If the value does not satisfy the predicate, we get a meaningful
// compile error:
scala> val i2: Int Refined Positive = -5
<console>:22: error: Predicate failed: (-5 > 0).
       val i2: Int Refined Positive = -5
like image 69
wheaties Avatar answered Nov 02 '22 10:11

wheaties


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:

Foo(_1)

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.

like image 37
flavian Avatar answered Nov 02 '22 12:11

flavian