Определить, известно ли значение выражения во время компиляции - PullRequest
0 голосов
/ 17 октября 2018

Предположим, я хочу создать тип 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

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

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

Ответы [ 2 ]

0 голосов
/ 18 октября 2018

совет сом-снитта, чтобы проверить 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 было просто неверным, так как все оценки былиобрабатываются путем встраивания (как подразумевается в комментарии Сом-Снитта).

0 голосов
/ 18 октября 2018

Если вы игнорируете макросы, то в 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 «ноль» и показываем, как он может преобразовать в целое число времени выполнения 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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...