4

Suppose I want to create a NonZero type so that my integer division function is total:

def div(numerator: Int, denominator: NonZero): Int =
  numerator / denominator.value

I can implement this by creating a NonZero class with a private constructor:

class NonZero private[NonZero] (val value : Int) { /*...*/ }

And a helper object to hold a Int => Option[NonZero] constructor, and an unapply so it can be used in match expressions:

object NonZero {
  def build(n:Int): Option[NonZero] = n match {
    case 0 => None
    case n => Some(new NonZero(n))
  }
  def unapply(nz: NonZero): Option[Int] = Some(nz.value)
  // ...
}

build is fine for runtime values, but having to do NonZero.build(3).get for literals feels ugly.

Using a macro, we can define apply only for literals, so NonZero(3) works, but NonZero(0) is a compile-time error:

object NonZero {
  // ...
  def apply(n: Int): NonZero = macro apply_impl
  def apply_impl(c: Context)(n: c.Expr[Int]): c.Expr[NonZero] = {
    import c.universe._
    n match {
      case Expr(Literal(Constant(nValue: Int))) if nValue != 0 =>
        c.Expr(q"NonZero.build(n).get")
      case _ => throw new IllegalArgumentException("Expected non-zero integer literal")
    }
  }
}

However this macro is less useful than it could be, as it only allows literals, not compile-time constant expressions:

final val X: Int = 3
NonZero(X) // compile-time error

I could pattern match on Expr(Constant(_)) in my macro, but then what about NonZero(X + 1)? I'd rather not have to implement my own scala expression evaluator.

Is there a helper or some easy way to determine if the value of an expression given to a macro is known at compile time (what C++ would call constexpr)?

rampion
  • 87,131
  • 49
  • 199
  • 315
  • 1
    The constant is written `final val X = 3` for inlining and constant folding. The type is ConstantType. There's `Toolbox.eval`. But maybe you want to inspect the definition of X, etc, hence a helper. Example with only constants https://github.com/scala/scala/blob/2.13.x/src/compiler/scala/tools/nsc/transform/UnCurry.scala#L603 – som-snytt Oct 17 '18 at 15:44

2 Answers2

0

If you ignore macros, then in Scala, only types exist at compile time, and only values exist at runtime. You can do type-level tricks to encode numbers as types at compile time, e.g. Type Level Programming in Scala

Here's a simplified version of the above Peano arithmetic example. First, we define a typeclass that shows how some type can convert to an integer.

@annotation.implicitNotFound("Create an implicit of type TValue[${T}] to convert ${T} values to integers.")
final class TValue[T](val get: Int) extends AnyVal

Then, we define the Peano 'zero' type and show how it can convert to a runtime integer 0:

case object TZero {
  implicit val tValue: TValue[TZero.type] = new TValue(0)
}

Then the Peano 'successor' type and how it can convert to a runtime integer 1 + previous value:

case class TSucc[T: TValue]()
object TSucc {
  implicit def tValue[TPrev](implicit prevTValue: TValue[TPrev]): TValue[TSucc[TPrev]] =
    new TValue(1 + prevTValue.get)
}

Then test safe division:

object Test {
  def safeDiv[T](numerator: Int, denominator: TSucc[T])(implicit tValue: TValue[TSucc[T]]): Int =
    numerator / tValue.get
}

Trying it out:

scala> Test.safeDiv(10, TZero)
<console>:14: error: type mismatch;
 found   : TZero.type
 required: TSucc[?]
       Test.safeDiv(10, TZero)
                        ^

scala> Test.safeDiv(10, TSucc[String]())
<console>:14: error: Create an implicit of type TValue[String] to convert String values to integers.
       Test.safeDiv(10, TSucc[String]())
                                     ^

scala> Test.safeDiv(10, TSucc[TZero.type]) // 10/1
res2: Int = 10

scala> Test.safeDiv(10, TSucc[TSucc[TZero.type]]) // 10/2
res3: Int = 5

As you can imagine though, this can get verbose fast.

Yawar
  • 11,272
  • 4
  • 48
  • 80
0

som-snytt's advice to check out ToolBox.eval led me to Context.eval, which the helper I'd been wanting:

object NonZero {
  // ...
  def apply(n: Int): NonZero = macro apply_impl
  def apply_impl(c: Context)(n: c.Expr[Int]): c.Expr[NonZero] = try {
    if (c.eval(n) != 0) {
      import c.universe._
      c.Expr(q"NonZero.build(n).get")
    } else {
      throw new IllegalArgumentException("Non-zero value required")
    }
  } catch {
    case _: scala.tools.reflect.ToolBoxError =>
      throw new IllegalArgumentException("Unable to evaluate " + n.tree + " at compile time")
  }
}

So now I can pass NonZero.apply constants and expressions made with constants:

scala> final val N = 3
scala> NonZero(N)
res0: NonZero = NonZero(3)
scala> NonZero(2*N + 1)
res1: NonZero = NonZero(7)
scala> NonZero(N - 3)
IllegalArgumentException: ...
scala> NonZero((n:Int) => 2*n + 1)(3))
IllegalArgumentException: ...

While it'd be nice if eval could handle pure functions like the last example above, this is good enough.

Embarrassingly, reviewing and retesting my earlier code from the question proved that my original macro handled the same expressions just as well!

My assertion that final val X = 3; NonZero(X) // compile-time error was just wrong, since all the evaluation was being handled by inlining (as som-snytt's comment implied).

rampion
  • 87,131
  • 49
  • 199
  • 315