Excelsior - макро аннотации
Feb. 19th, 2013
10:07 pm - макро аннотации
Да, это именно то, о чем вы подумали. На выходных наконец-то пришло озарение на тему того, как отрефакторить текущую архитектуру тайпчекера, чтобы он позволял менять произвольные классы в процессе компиляции. Это означает, что теперь становится возможным тотальное безрассудство вроде следующего:
13:29 ~/Projects/Kepler_introduce-member/sandbox$ cat Macros.scala
import scala.reflect.macros.Context
import language.experimental.macros
object Macros {
def impl(c: Context)(target: c.Tree, name: c.Tree, code: c.Tree) = {
import c.universe._
val Literal(Constant(targetType: Type)) = c.typeCheck(target)
val Literal(Constant(methodName: String)) = name
val Function(methodParams, methodBody) = code
val method = DefDef(NoMods, TermName(methodName), Nil, List(methodParams), TypeTree(), methodBody)
c.introduceMember(targetType.typeSymbol, method)
c.literalUnit
}
def addMethod(target: _, name: String, code: _) = macro impl
}
13:29 ~/Projects/Kepler_introduce-member/sandbox$ cat Test.scala
class C
object Test extends App {
Macros.addMethod(classOf[C], "foo", (x: Int) => x + 2)
println(new C().foo(2))
}
13:29 ~/Projects/Kepler_introduce-member/sandbox$ scalac Macros.scala && scalac Test.scala && scala Test
4Если вкратце, в scala.reflect.macros.Context я добавил метод introduceMember, который может в любой компилируемый в текущий момент класс (а также трейт или объект) добавить любой мембер (метод, поле, вложенный класс и т.д.). То, что уже скомпилировано в байткод, менять, конечно, не получится - мы ж тут не магией занимаемся, в конце концов. Впрочем, это еще цветочки. В принципе, субботнее озарение позволяет реализовать не только скромное добавление новых мемберов, но и изменение и удаление старых, переколбас компаньонов и так далее.А теперь вопрос. Вот есть полная свобода метапрограммирования, описанная выше. Если честно, я немного в растерянности. Вообще-то, с этой недели я планировал забить на время на девелопмент и сесть за написание паперы, а оно на выходных само взяло и наколбасилось. Что теперь с этой свободой делать? Что бы вы реализовали в первую очередь?
находить в коде "([0-9a-f]{40})".r и заменять на скомпилированный код.
то есть создавать по object'у для каждой уникальной строки с регексом.
заодно выкинув дубликаты.
поспрашивай у немерлистов -- какие у них полезные макросы
а я как всегда выкачу обратный пример: система с 2 макросами
М1: добавить в класс С1 метод foo при условии, что в С2 есть метод bar
М2: добавить в класс С2 метод bar при условии, что в С1 есть метод foo
обладает 2 стабильными состояниями и не совсем ясно, какое из них "верное"
если же макросы запускаются исходя из порядка компиляции, получаем вообще х.з. что [чтобы получить х.з. что, надо М1 переформулировать так: добавить в класс С1 метод foo при условии, что в С2 есть метод bar или в классе С3 есть метод baz, а М2 оставить как есть]
таким образом утрачивается как минимум комфорт обычного языка программирования -- независимость от порядка компиляции; как предполагается решать этот вопрос?
Edited at 2013-02-20 06:23 am (UTC)
в макросистеме плюсов (если что, я не о #define, а о шаблонах) макросы М1 и М2 можно определить, но не получится "одновременно" включить их оба в один файл, т.к. плюсы требуют предварительно объявлять класс, а у предварительно объявленного класса невозможно узнать наличие/отсутствие методов (и полей) -- т.е. М1 и М2 придется ручками упорядочить, причем первый из них не сможет работать
по макросам по-моему достаточно уже высказал свою точку зрения -- я не буду участвовать в их развитии, но буду интересоваться, что получилось сделать с их помощью полезного; идеальным был бы чейнджлог типа "сделан такой-то макрос для такой-то задачи (1-2 абзаца про задачу и почему без макроса получалось плохо), подробности вот здесь"
еще интересен лог вида "вот такая-то задача требует неприятного boilerplate-а и возможно хорошо решиться макросами" (тоже 1-2 абзаца + кат или ссылка)
[не обязательно это форматировать именно в виде лога -- можно например в виде жж поста с 2 абзацами и разъяснением под катом; важно только чтобы можно было за этим следить, не углубляясь под кат и в комменты -- т.е. чтобы ты выносил полезные идеи из комментов в отдельные посты; возможно, ты захочешь это делать сразу по английски, но не знаю как тебе, а мне по-русски существенно комфортнее/легче писать]
[btw еще обрати внимание, насколько полезно вытаскивать коммент из середины в отдельный пост на примере "так ли нужна вся мощь макросов"]
мне лучше заняться изучением/обсуждением скалы (когда ты напишешь с какими тараканами приходится дружить, чем мешает перегрузка сигнатур и прочее)
еще мне интересно было бы изучить
китайский придворный этикетметапрограммирование на языке имплицитов; думаю, тебе было бы полезно изучить поглубже этот вопрос, чтобы выкатывать use case-ы вида "вот на имплицитах это реализуется вот так, а на макросах вот так -- профит!"тут видимо нужен некий cheat sheet -- как делать присваивание, цикл (или рекурсию), добавлять поля/методы к классу и т.п.
и вообще в целом я больше тут не писатель, а читатель
Edited at 2013-02-21 04:18 am (UTC)
пока ты так не прокоментировал, мне было непонятно зачем смотреть
тогда да, если компилятор не давал вообще ничего инжектить -- это было плохо
что же касается обоих примеров -- первый, который я не понимаю и с невидимым объектом и второй, с atomic long -- то похоже инжектить ничего не надо
по 1 -- без невидимых объектов вроде как можно обойтись
по 2 -- вместо того чтобы инжектить рядом с лонгом этот лонг *заменяется* на структурку с геттером лонга и приватным для структурки AtomicLong
а *в общем случае* инжектить похоже да, придется -- скажем, когда захочется инжектить в статический компаньон из-за аннотации, подвешанной на нестатический метод или нестатическое поле
btw, а почему одерский сделал статические компаньоны? моя гипотеза -- чтобы они собирались сборщиком мусора, т.к. обычные статические объекты не собираются (в смысле, если больше не осталось объектов класса, и никто не указывает на экземпляр статического компаньона, состоящего из скажем небоксированных целых, то сборщик может его собрать, а в случае просто статического размещения сборщик собрать его не сможет)
Edited at 2013-02-21 10:06 am (UTC)
Соответственно, на единственный инстанс C$ будет указывать статический филд в классе C, т.е. он всегда будет достижим из gc roots.object C class C$ {} class C { static C$ MODULE$ = new C$ }Edited at 2013-02-21 10:52 am (UTC)
так что мы и сейчас отлично беседуем
Мдя-с... :-/
скажем, собеседник может иметь в виду например следующее:
у нас имеются два *текстуально* разных метода (которые на самом деле являются *ручными* оптимизациями одного и того же кода), и эти два *текстуально* разных метода в двух файлах называются одним и тем же именем, т.к. результат их работы идентичен, но скорость работы заточена под место вызова
это, с моей точки зрения, abuse, но при этом такая идея обсуждабельна
возможен еще более закрученный вариант, когда результат работы обоих методов не идентичен априори, но становится идентичным, если их вызывать только из правильного модуля (т.к. у каждого модуля свой инвариант, и методы заинлайнены руками под правильный инвариант)
Edited at 2013-02-20 11:31 am (UTC)
спасибо, к.о., именно с констатации моего непонимания я и начал
> Чтобы "понять собеседника" нужно всего лишь иметь немного опыта реальных промышленных проектов и общения с реальными программистами.
похоже тебе попадались слишком одинаковые "реальные программисты"
жабка, кстати, отнюдь не в последнюю очередь выбилась вперед из-за того, что к ней можно подпускать индусов самым безопасным среди прочих языков образом
жабка была тихим ужасом вплоть до 1.5, когда появились дженерики -- тогда она хотя бы с виду стала похожа на что-то приличное, хотя до сих пор она именно что только похожа
Если подробнее, для f вот здесь:
case class Shift[T, RR, SR](fun: (T => RR) => SR) { // RR = RegularResult, SR = ShiftedResult def map [X](f: T => X) = Shift { k: (X => RR) => fun { a => k(f(a)) } } def flatMap[X](f: T => Shift[X, RR, RR]) = Shift { k: (X => RR) => fun { a => f(a).fun(k) } } } def f(x: Int): Int @cpsParam[Int, Int] = { println(1) val y: Int = shift { (k: Int => Int) => k(2) + 1 } println(3) y * 2 }генерировать сбоку
def f_pure(x: Int): Shift[Int, Int, Int] = { println(1) Shift { k: (Int => Int) => k(2) + 1 } map { yy => val y = yy println(3) y * 2 } }и использовать потом в дальнейших макропреобразованиях внутри макроса cps (и, через него в reify) для подмен вызовов f вызовами f_pure. Сам f нужен, чтобы код оттипизировался (совместимость Shift[Int, Int, Int] с Int через Int @cpsParam[Int, Int], но с сохранением информации, позволяющей восстановить первое). Без этого только хакать тайпчекер плагином.
Только по мне, прикольнее были бы всё-таки не произвольные вклинивания совсем, а именно аннотации, область действия которых ограничена аннотируемой сущностью. Даже при обычном множественном наследовании часто возникает проблема понять, откуда прилетает такой-то член или метод (особенно, без scala-ide), а так будет возможность писать классы, члены в которых берутся откуда-то неотслеживаемым образом.
Edited at 2013-02-20 09:13 am (UTC)
Касательно области действия аннотаций все не так просто. Вот уже в твоем примере @cpsParam совсем не ограничен аннотируемым ретурн тайпом метода, а должен иметь возможность влиять на контейнер функции.
Ну необязательно. Наверное, в моём случае правильнее будет генерировать f_pure в каком-нибудь вспомогательном сгенерированном объекте.
Вот одна из возможных простеньких и понятных стратегий:
Аннотации на методы - анализ тела метода, переписывание тела метода
аннотации на классы - анализ тела класса, переписывание тела класса
и т.д.
При этом можно разрешать произвольную кодогенерацию в каких-то невидимых объектах для внутреннего использования, интерфейс которых никак не будет затрагивать пользователя. Когда пользователь видит метод, но не имеет возможности найти, какой именно макрос из юнита компиляции его сгенерировал, это явно плохо. А вот когда метод используется внутреннее, пользователь его не будет использовать непосредственно, не будет знать о его существовании, и у него не возникнет надобности узнавать откуда он взялся - это вроде бы и нормально.
Еще пример. Хотим мы реализовать аннотацию @atomic, которая обеспечивает атомарность апдейтов лонгов. Для обеспечения ее работы рядом с полем, помеченным аннотацией, нужно будет положить что-то вспомогательное, например, AtomicLong.
Можно в один и тот же private модуль внутри класса. В случае пожара, можно будет посмотреть всё вспомогательное, нагенерированное из аннотаций, отрефлексировав этот модуль.
Второй пример, если честно, не совсем понимаю, но можно сделать макроаннотацию на класс @WithAtomicGenerator("G"), которая генерит рядом со всеми полями, помеченными @apply("G"), какие-то AtomicLong. Вроде, так более-менее прозрачно всё даже для тех, кто не знает, как работает WithAtomicGenerator, но вкурсе правил на переписывание. Так ему будет понятно, что от связи аннотации на класс такой структуры с аннотациями на методы стоит ожидать появления новых полей.