Операции во время компиляции
Пакет scala.compiletime
Пакет scala.compiletime
содержит операции метапрограммирования, которые можно использовать внутри inline
метода.
Эти операции охватывают некоторые распространенные случаи использования макросов без необходимости определения макроса.
Reporting
Метод error
используется для создания определяемых пользователем ошибок компиляции во время встроенного расширения.
Он имеет следующую подпись:
inline def error(inline msg: String): Nothing
Если встроенное расширение приводит к вызову error(msgStr)
,
компилятор выдает сообщение об ошибке, содержащее заданный msgStr
.
import scala.compiletime.error
inline def doSomething(inline mode: Boolean): Unit =
if mode then println("true")
else if !mode then println("false")
else error("Mode must be a known value")
doSomething(true)
// true
doSomething(false)
// false
val bool: Boolean = true
doSomething(bool)
// error:
// Mode must be a known value
Если error
вызывается вне inline
метода, при компиляции этого вызова будет выдаваться ошибка.
Если error
написан внутри inline
метода, ошибка будет выдаваться только в том случае,
если после встраивания вызова он не будет удален как часть мертвой ветки.
В предыдущем примере, если бы значение mode
было известно во время компиляции,
сохранена была бы только одна из первых двух ветвей и ошибки компиляции не было бы.
Если желательно включить часть исходного кода аргументов в сообщение об ошибке,
можно использовать метод codeOf
.
import scala.compiletime.{codeOf, error}
inline def doSomething(inline mode: Boolean): Unit =
if mode then println("true")
else if !mode then println("false")
else error("Mode must be a known value but got: " + codeOf(mode))
val bool: Boolean = true
doSomething(bool)
// |doSomething(bool)
// |^^^^^^^^^^^^^^^^^
// |Mode must be a known value but got: bool
Выборочный вызов имплицитов (Summoning)
summonFrom
Предполагается, что многие области программирования на уровне типов
могут выполняться с помощью методов перезаписи вместо implicits.
Но иногда implicits неизбежны.
До сих пор проблема заключалась в том, что стиль программирования неявного поиска,
подобный Prolog, становится вирусным:
как только некоторая конструкция зависит от неявного поиска,
она сама должна быть написана как логическая программа.
Рассмотрим, например, проблему создания TreeSet[T]
или HashSet[T]
в зависимости от того,
имеет ли тип T
Ordering
или нет.
Мы можем создать набор неявных определений следующим образом:
trait SetFor[T, S <: Set[T]]
class LowPriority:
implicit def hashSetFor[T]: SetFor[T, HashSet[T]] = ...
object SetsFor extends LowPriority:
implicit def treeSetFor[T: Ordering]: SetFor[T, TreeSet[T]] = ...
Понятно, что это некрасиво.
Помимо всей обычной косвенности неявного поиска, мы сталкиваемся с проблемой определения приоритетов правил,
когда мы должны гарантировать, что treeSetFor
имеет приоритет над hashSetFor
, если тип элемента имеет Ordering
.
Это решается (неуклюже) путем добавления hashSetFor
в суперкласс LowPriority
объекта SetsFor
,
где определен treeSetFor
.
Возможно, шаблон все еще был бы приемлем, если бы можно было сдержать грубый код.
Однако, это не так.
Каждый пользователь абстракции должен быть параметризован с помощью имплицитного SetFor
.
Принимая во внимание простую задачу "Я хочу TreeSet[T]
, если T
можно упорядочить, иначе — HashSet[T]
",
это кажется слишком церемонным.
Есть некоторые предложения по улучшению ситуации в конкретных областях, например, путем разрешения более сложных схем определения приоритетов. Но все они сохраняют вирусный характер программ неявного поиска, основанных на логическом программировании.
Напротив, новая конструкция summonFrom
делает неявный поиск доступным в функциональном контексте.
Чтобы решить проблему создания правильного набора, можно было бы использовать его следующим образом:
import scala.compiletime.summonFrom
inline def setFor[T]: Set[T] = summonFrom {
case ord: Ordering[T] => new TreeSet[T]()(using ord)
case _ => new HashSet[T]
}
Вызов summonFrom
принимает pattern matching в качестве аргумента.
Все паттерны в замыкании являются атрибуциями типа формы identifier : Type
.
Паттерны примеряются последовательно.
Выбирается первый паттерн x: T
, при котором может быть вызвано неявное значение типа T
.
В качестве альтернативы можно также использовать given
экземпляр с привязкой к шаблону,
что позволяет избежать явного предложения using
.
Например, setFor
можно было бы также сформулировать следующим образом:
import scala.compiletime.summonFrom
inline def setFor[T]: Set[T] = summonFrom {
case given Ordering[T] => new TreeSet[T]
case _ => new HashSet[T]
}
summonFrom
приложения должны быть уменьшены во время компиляции.
Следовательно, если мы вызовем Ordering[String]
, то вернется новый экземпляр TreeSet[String]
.
summon[Ordering[String]]
println(setFor[String].getClass) // prints class scala.collection.immutable.TreeSet
Отметим, приложения summonFrom
могут вызывать ambiguity errors.
Рассмотрим следующий код с двумя given
в области видимости типа A
.
Совпадение с шаблоном f
вызовет ошибку неоднозначности f
.
class A
given a1: A = new A
given a2: A = new A
inline def f: Any = summonFrom {
case given _: A => ??? // error: ambiguous givens
}
summonInline
Сокращение summonInline
обеспечивает простой способ написать summon
,
который откладывается до тех пор, пока вызов не будет встроен.
В отличие от summonFrom
, summonInline
также выдает ошибку неявного отсутствия (implicit-not-found error),
если данный экземпляр вызываемого типа не найден.
import scala.compiletime.summonInline
import scala.annotation.implicitNotFound
@implicitNotFound("Missing One")
trait Missing1
@implicitNotFound("Missing Two")
trait Missing2
trait NotMissing
given NotMissing = ???
transparent inline def summonInlineCheck[T <: Int](inline t : T) : Any =
inline t match
case 1 => summonInline[Missing1]
case 2 => summonInline[Missing2]
case _ => summonInline[NotMissing]
val missing1 = summonInlineCheck(1) // error: Missing One
val missing2 = summonInlineCheck(2) // error: Missing Two
val notMissing : NotMissing = summonInlineCheck(3)
Values
constValue и constValueOpt
constValue
- это функция, которая производит постоянное значение, представленное типом.
import scala.compiletime.constValue
import scala.compiletime.ops.int.S
transparent inline def toIntC[N]: Int =
inline constValue[N] match
case 0 => 0
case _: S[n1] => 1 + toIntC[n1]
inline val ctwo = toIntC[2]
constValueOpt
- то же самое, что и constValue
, но возвращает Option[T]
,
что позволяет обрабатывать ситуации, когда значение отсутствует.
Обратите внимание, что S
- это тип преемника некоторого одноэлементного типа.
Например, S[1]
- это одноэлементный тип 2
.
Inline Matching
erasedValue
До сих пор были показаны встроенные (inline
) методы,
которые принимают термины (кортежи и целые числа) в качестве параметров.
Что, если вместо этого необходимо основывать различия на типах?
Например, хотелось бы иметь возможность написать функцию defaultValue
,
которая для заданного типа T
возвращает Option
значение по умолчанию для T
, если оно существует.
Можно выразить это, используя переписывающие выражения соответствия
и простую вспомогательную функцию scala.compiletime.erasedValue
, которая определяется следующим образом:
def erasedValue[T]: T
Функция erasedValue
делает вид, что возвращает значение аргумента типа T
.
Вызов этой функции всегда будет приводить к ошибке времени компиляции,
если вызов не будет удален из кода при встраивании.
Используя erasedValue
, можно определить defaultValue
следующим образом:
import scala.compiletime.erasedValue
transparent inline def defaultValue[T] =
inline erasedValue[T] match
case _: Byte => Some(0: Byte)
case _: Char => Some(0: Char)
case _: Short => Some(0: Short)
case _: Int => Some(0)
case _: Long => Some(0L)
case _: Float => Some(0.0f)
case _: Double => Some(0.0d)
case _: Boolean => Some(false)
case _: Unit => Some(())
case _ => None
Затем:
val dInt: Some[Int] = defaultValue[Int]
// dInt: Some[Int] = Some(value = 0)
val dDouble: Some[Double] = defaultValue[Double]
// dDouble: Some[Double] = Some(value = 0.0)
val dBoolean: Some[Boolean] = defaultValue[Boolean]
// dBoolean: Some[Boolean] = Some(value = false)
val dAny: None.type = defaultValue[Any]
// dAny: None = None
В качестве другого примера рассмотрим приведенную ниже версию на уровне типов toInt
:
для заданного типа, представляющего число Пеано, вернуть соответствующее ему целочисленное значение.
Рассмотрим определения чисел, как в разделе "inline match".
Вот как можно определить toIntT
:
transparent inline def toIntT[N <: Nat]: Int =
inline scala.compiletime.erasedValue[N] match
case _: Zero.type => 0
case _: Succ[n] => toIntT[n] + 1
inline val two = toIntT[Succ[Succ[Zero.type]]]
erasedValue
- это erased
метод, поэтому его нельзя использовать и он не имеет поведения во время выполнения.
Поскольку toIntT
выполняет статические проверки статического типа N
,
его можно безопасно использовать для тщательного изучения возвращаемого типа (в данном случае - S[S[Z]]
).
Операции (scala.compiletime.ops)
Пакет scala.compiletime.ops
содержит типы, обеспечивающие поддержку примитивных операций над одноэлементными типами.
Например, scala.compiletime.ops.int.*
обеспечивает поддержку умножения двух одноэлементных типов Int
,
scala.compiletime.ops.boolean.&&
- объединения двух Boolean
типов.
Когда все аргументы типа scala.compiletime.ops
являются одноэлементными типами,
компилятор может оценить результат операции.
import scala.compiletime.ops.int.*
import scala.compiletime.ops.boolean.*
val conjunction: true && true = true
val multiplication: 3 * 5 = 15
Многие из этих одноэлементных операций предназначены для использования в инфиксе (как в SLS §3.2.10).
Поскольку псевдонимы типов имеют те же правила приоритета, что и их эквиваленты на уровне терминов, операции составляются с ожидаемыми правилами приоритета:
import scala.compiletime.ops.int.*
val x: 1 + 2 * 3 = 7
Типы операций расположены в пакетах, названных по типу левого параметра:
например, scala.compiletime.ops.int.+
представляет собой сложение двух чисел,
а scala.compiletime.ops.string.+
представляет собой конкатенацию строк.
Чтобы использовать оба типа и отличить их друг от друга,
тип соответствия может отправляться в правильную реализацию:
import scala.compiletime.ops.*
type +[X <: Int | String, Y <: Int | String] = (X, Y) match
case (Int, Int) => int.+[X, Y]
case (String, String) => string.+[X, Y]
val concat: "a" + "b" = "ab"
val addition: 1 + 1 = 2
Детали
Дополнительная информация об операциях во время компиляции:
Ссылки: