Определить, известно ли значение выражения во время компиляции

Предположим, я хочу создать тип NonZero, чтобы моя функция целочисленного деления была полной:

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

Я могу реализовать это, создав класс NonZero с приватным конструктором:

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

И вспомогательный объект для хранения конструктора Int => Option[NonZero] и unapply, чтобы его можно было использовать в выражениях match:

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 подходит для значений времени выполнения, но необходимость делать NonZero.build(3).get для литералов кажется уродливой.

Используя макрос, мы можем определить apply только для литералов, поэтому NonZero(3) работает, но NonZero(0) является ошибкой времени компиляции:

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

Однако этот макрос менее полезен, чем мог бы быть, поскольку он допускает только литералы, а не постоянные выражения времени компиляции:

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

Я может найти шаблон для Expr(Constant(_)) в моем макросе, но как насчет NonZero(X + 1)? Я бы предпочел не реализовывать свой собственный оценщик выражений scala.

Есть ли помощник или какой-нибудь простой способ определить, известно ли значение выражения, переданного макросу, во время компиляции (что С++ вызовет constexpr)?


person rampion    schedule 17.10.2018    source источник
comment
Константа записывается final val X = 3 для встраивания и сворачивания констант. Тип — константный тип. Есть Toolbox.eval. Но, может быть, вы хотите проверить определение X и т. Д., Следовательно, помощник. Пример только с константами github.com/scala/scala/blob/2.13.x/src/compiler/scala/tools/nsc/   -  person som-snytt    schedule 17.10.2018


Ответы (2)


Если игнорировать макросы, то в Scala во время компиляции существуют только типы, а во время выполнения — только значения. Вы можете делать трюки на уровне типов, чтобы кодировать числа как типы во время компиляции, например. Программирование на уровне типов в Scala

Вот упрощенная версия приведенного выше примера арифметики Пеано. Во-первых, мы определяем класс типов, который показывает, как некоторый тип может быть преобразован в целое число.

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

Затем мы определяем тип Peano 'zero' и показываем, как его можно преобразовать в целое число 0 во время выполнения:

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

Затем тип «преемника» Peano и то, как его можно преобразовать в целое число времени выполнения 1 + предыдущее значение:

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

Затем проверьте безопасное деление:

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

Пробуем:

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

Однако, как вы можете себе представить, это может быстро стать многословным.

person Yawar    schedule 18.10.2018

совет som-snytt проверить ToolBox.eval привел меня к Context.eval, который мне давно нужен:

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

Итак, теперь я могу передать NonZero.apply константы и выражения, созданные с помощью констант:

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

Хотя было бы неплохо, если бы eval мог обрабатывать чистые функции, как в последнем примере выше, этого достаточно.

К сожалению, просмотр и повторное тестирование моего предыдущего кода из вопроса доказали, что мой исходный макрос так же хорошо обрабатывал те же выражения!

Мое утверждение о том, что final val X = 3; NonZero(X) // compile-time error было просто неверным, так как вся оценка выполнялась путем встраивания (как подразумевал комментарий som-snytt).

person rampion    schedule 18.10.2018