Статьи

Начиная с Scala Macros: краткий учебник

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

Макросы также написаны на Scala, поэтому, по сути, макрос — это фрагмент кода Scala, выполняемый во время компиляции, который манипулирует и изменяет AST программы Scala.

Чтобы сделать что-то полезное, я хотел реализовать простой макрос для отладки; Я предполагаю, что я не одинок в использовании println-debugging, то есть отладки, вставляя такие выражения:

1
println('After register; user = ' + user + ', userCount = ' + userCount)

запустить тест и проверить, что вывод. Запись имени переменной перед переменной утомительна, поэтому я хотел написать макрос, который бы сделал это для меня; это:

1
debug('After register', user, userCount)

должен иметь тот же эффект, что и первый фрагмент (он должен генерировать код, аналогичный приведенному выше).

Давайте посмотрим шаг за шагом, как реализовать такой макрос. На странице макросов scala есть хорошее руководство по началу работы, которое я использовал. Весь код, описанный ниже, доступен на GitHub, в проекте scala-macro-debug .

1. Настройка проекта

Чтобы комфортно поэкспериментировать, нам нужно сначала настроить простой проект. Нам понадобится как минимум два подпроекта: один для макросов и один для тестирования макросов. Это потому, что макросы должны быть скомпилированы отдельно и до, и код, который их использует (поскольку они влияют на процесс компиляции).

Более того, подпроект макроса должен иметь зависимость от scala-компилятора, чтобы иметь доступ к классам отражения и AST.

Простой файл сборки SBT может выглядеть так: Build.scala .

2. Привет, мир!

«Привет мир!» всегда отличная отправная точка. Поэтому мой первый шаг — написать макрос, который расширил бы hello() до println('Hello World!') Во время компиляции.

В подпроекте макросов мы должны создать новый объект, который определяет hello() и макрос:

01
02
03
04
05
06
07
08
09
10
11
12
13
package com.softwaremill.debug
 
import language.experimental.macros
 
import reflect.macros.Context
 
object DebugMacros {
  def hello(): Unit = macro hello_impl
 
  def hello_impl(c: Context)(): c.Expr[Unit] = {
    // TODO
  }
}

Здесь есть пара важных вещей:

  1. нам нужно импортировать language.experimental.macros , чтобы включить функцию макросов в данном исходном файле. В противном случае мы получим ошибки компиляции, напоминающие нам об импорте.
  2. определение hello() использует ключевое слово macro , за которым следует метод, который реализует макрос
  3. Реализация макроса имеет два списка параметров: первый — это контекст (вы можете думать об этом как о контексте компиляции), а второй отражает список параметров нашего метода — здесь он пуст. Наконец, возвращаемый тип также должен совпадать — однако в методе у нас есть единица возвращаемого типа, в макросе мы возвращаем выражение (которое оборачивает часть AST) единицы типа.

Теперь о реализации, которая довольно коротка:

1
2
3
4
def hello_impl(c: Context)(): c.Expr[Unit] = {
  import c.universe._
  reify { println('Hello World!') }
}

Идя строка за строкой:

  1. Сначала мы импортируем «юниверс», который предоставляет удобный доступ к классам AST. Обратите внимание, что тип возвращаемого значения — c.Expr — поэтому это тип, зависящий от пути, взятый из контекста. Вы увидите этот импорт в каждом макросе.
  2. поскольку мы хотим сгенерировать код, который печатает «Hello World!», нам нужно создать для него AST. Вместо того чтобы создавать его вручную (что возможно, но выглядит не очень красиво), Scala предоставляет метод reify (reify также является макросом — макрос, используемый при компиляции макросов), который превращает данный код в Expr[T] (выражения обернуть AST и его тип). Так как println имеет тип unit, выражение Expr[Unit] имеет тип Expr[Unit] , и мы можем просто вернуть его.

Использование довольно просто. В подпроекте тестирования напишите следующее:

1
2
3
4
object DebugExample extends App {
  import DebugMacros._
  hello()
}

и запустите код (например, с помощью команды run в оболочке SBT).

3. Распечатка параметра

Печать Hello World хороша, но еще приятнее напечатать параметр. Второй макрос сделает именно это: он преобразует printparam(anything) в println(anything) . Не очень полезно и похоже на то, что мы видели, с двумя принципиальными отличиями:

1
2
3
4
5
6
def printparam(param: Any): Unit = macro printparam_impl
 
def printparam_impl(c: Context)(param: c.Expr[Any]): c.Expr[Unit] = {
  import c.universe._
  reify { println(param.splice) }
}

Первое отличие состоит в том, что метод принимает параметр param: Any . В реализации макроса мы должны отразить это — но так же, как и с типом возврата, вместо Any мы принимаем Expr[Any] , как во время компиляции мы работаем с AST.

Второе отличие заключается в использовании splice . Это специальный метод Expr , который может использоваться только внутри вызова reify , и делает нечто вроде обратного reify: он встраивает данное выражение в код, который подвергается reify. Здесь у нас есть param который является Expr (то есть tree + type), и мы хотим поместить это дерево как дочерний элемент println ; мы хотим, чтобы значение, представленное param передавалось в println , а не в AST. Expr[T] вызванный на Expr[T] возвращает T , поэтому проверенный тип проверяет тип.

4. Отладка одной переменной

Давайте теперь перейдем к нашему методу отладки. Сначала, может быть, давайте реализуем отладку с одной переменной, то есть debug(x) должен быть преобразован во что-то вроде println('x = ' + x) .

Вот макрос:

1
2
3
4
5
6
7
8
9
def debug(param: Any): Unit = macro debug_impl
 
def debug_impl(c: Context)(param: c.Expr[Any]): c.Expr[Unit] = {
  import c.universe._
  val paramRep = show(param.tree)
  val paramRepTree = Literal(Constant(paramRep))
  val paramRepExpr = c.Expr[String](paramRepTree)
  reify { println(paramRepExpr.splice + ' = ' + param.splice) }
}

Новым, конечно, является генерация префикса. Чтобы сделать это, мы сначала превращаем дерево параметра в String . Встроенный метод show делает именно это. Небольшая заметка здесь; так как мы превращаем AST в String , вывод может выглядеть немного иначе, чем в исходном коде. Для vals, объявленных внутри метода, он вернет просто имя val. Для полей класса вы увидите что-то вроде DebugExample.this.myField . Для выражений, например, left + right , вы увидите left.+(right) . Не идеальный, но достаточно читаемый, я думаю.

Во-вторых, нам нужно создать дерево (на этот раз вручную), представляющее константу String . Здесь вы просто должны знать, что строить, например, проверяя деревья, созданные путем реификации (или читая исходный код компилятора Scala).

Наконец, мы превращаем это простое дерево в выражение типа String и склеиваем его внутри println . Запустив например такой код:

01
02
03
04
05
06
07
08
09
10
11
12
13
object DebugExample extends App {
  import DebugMacros._
 
  val y = 10
 
  def test() {
    val p = 11
    debug1(p)
    debug1(p + y)
  }
 
  test()
}

выходы:

1
2
p = 11
p.+(DebugExample.this.y) = 21

5. Конечный продукт

Реализация полного макроса отладки, как описано выше, вводит только одну новую концепцию. Полный исходный код немного длинный, поэтому вы можете просмотреть его на GitHub .

В реализации макроса мы сначала генерируем дерево (AST) для каждого параметра, который представляет либо печать константы, либо выражение. Затем мы чередуем деревья с разделителями ( ', ' ) для удобства чтения.

Наконец, мы должны превратить список деревьев в выражение. Для этого мы создаем Block . Блок принимает список операторов, которые должны быть выполнены, и выражение, которое является результатом всего блока. В нашем случае результат, конечно, () .

И теперь мы можем счастливо отлаживать! Например, написание:

1
debug('After register', user, userCount)

напечатает, когда выполнено:

1
AfterRegister, user = User(x, y), userCount = 1029

Подводя итоги

Это довольно длинный пост, рад, что кто-то сделал это так далеко. В любом случае, макросы выглядят действительно интересно, и довольно просто начать писать макросы самостоятельно. Вы можете найти простой проект SBT плюс код, обсуждаемый здесь на GitHub ( проект scala-macro-debug ). И я полагаю, что скоро мы увидим выход проектов макропередач. Уже есть некоторые, например, Expecty или Macrocosm .

Ссылка: Начиная со Scala Macros: краткое руководство от нашего партнера по JCG Адама Варски в