Статьи

Начиная с макросов Scala: краткое руководство

Используя некоторое время , в выходные дни, я решил , наконец , изучить один новые возможности в ближайшем Scala 2,10, макросы . Макросы также написаны на Scala, поэтому, по сути, макрос — это фрагмент кода Scala, выполняемый во время компиляции, который манипулирует и изменяет AST программы Scala.

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

println("After register; user = " + user + ", userCount = " + userCount)

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

debug("After register", user, userCount)

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

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

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

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

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

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

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

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

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

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) единицы типа.

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

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](перенос выражений) АСТ и его тип). Как и в случае printlnс типом unit, выражение reified имеет тип Expr[Unit], и мы можем просто вернуть его.

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

object DebugExample extends App {
  import DebugMacros._
  hello()
}

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

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

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

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: он встраивает данное выражение в код, который подвергается реификации. Здесь мы имеем, paramкоторый является Expr(то есть, дерево + тип), и мы хотим поместить это дерево как потомок println; мы хотим paramпередать значение, которое представлено println, а не AST. spliceвызывается по Expr[T]возврату a T, поэтому проверенный тип проверяет тип

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

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

Вот макрос:

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 в a String, вывод может выглядеть немного иначе, чем в исходном коде. Для vals, объявленных внутри метода, он вернет просто имя val. Для полей класса вы увидите что-то вроде DebugExample.this.myField. Для выражений, например left + right, вы увидите left.+(right). Не идеальный, но достаточно читаемый, я думаю.

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

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

object DebugExample extends App {
  import DebugMacros._
 
  val y = 10
 
  def test() {
    val p = 11
    debug1(p)
    debug1(p + y)
  }
 
  test()
}

выходы:

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

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

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

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

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

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

debug("After register", user, userCount)

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

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

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

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