You are viewing xeno_by

Excelsior - макро аннотации

Feb. 19th, 2013

10:07 pm - макро аннотации

Previous Entry Add to Memories Share Next Entry

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

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, который может в любой компилируемый в текущий момент класс (а также трейт или объект) добавить любой мембер (метод, поле, вложенный класс и т.д.). То, что уже скомпилировано в байткод, менять, конечно, не получится - мы ж тут не магией занимаемся, в конце концов. Впрочем, это еще цветочки. В принципе, субботнее озарение позволяет реализовать не только скромное добавление новых мемберов, но и изменение и удаление старых, переколбас компаньонов и так далее.

А теперь вопрос. Вот есть полная свобода метапрограммирования, описанная выше. Если честно, я немного в растерянности. Вообще-то, с этой недели я планировал забить на время на девелопмент и сесть за написание паперы, а оно на выходных само взяло и наколбасилось. Что теперь с этой свободой делать? Что бы вы реализовали в первую очередь?

Comments:

From:q987
Date:February 19th, 2013 09:22 pm (UTC)
(Link)
кажется, раньше это звалось аспектно ориентированное програмирование, не ?
(Reply) (Thread)
[User Picture]
From:xeno_by
Date:February 19th, 2013 09:28 pm (UTC)
(Link)
похоже, да
(Reply) (Parent) (Thread)
From:M E
Date:February 20th, 2013 10:46 am (UTC)
(Link)
емнип AOP это немного больше -- если у нас есть метод do_something, то AOP позволяет в самую первую его строку добавить вызов check_that_user_is_authorised
(Reply) (Parent) (Thread)
[User Picture]
From:xeno_by
Date:February 20th, 2013 11:08 pm (UTC)
(Link)
Если есть introduceMember, то легко наколбасить и editMember, который делает то, о чем говорится, возможным. Разве что байткод макросы поменять не смогут.
(Reply) (Parent) (Thread)
From:zhengxi
Date:February 19th, 2013 09:23 pm (UTC)
(Link)
не очень важное, но красивое:

находить в коде "([0-9a-f]{40})".r и заменять на скомпилированный код.
то есть создавать по object'у для каждой уникальной строки с регексом.
(Reply) (Thread)
[User Picture]
From:den_sh
Date:February 19th, 2013 09:32 pm (UTC)
(Link)
В 2.10 для этого намного больше подходит строковая интерполяция (http://docs.scala-lang.org/overviews/core/string-interpolation.html). С помощью нее можно например определить r"" интерполятор, который будет компилировать регулярки во время компиляции кода.
(Reply) (Parent) (Thread)
[User Picture]
From:xeno_by
Date:February 19th, 2013 09:39 pm (UTC)
(Link)
Автор камента имел ввиду, что r"" может быть макросом.
(Reply) (Parent) (Thread)
From:zhengxi
Date:February 19th, 2013 09:58 pm (UTC)
(Link)
и еще то, что теперь в макросе можно создавать объекты (или методы в глобальном объекте), и код вынести туда.
заодно выкинув дубликаты.
(Reply) (Parent) (Thread)
From:M E
Date:February 19th, 2013 11:53 pm (UTC)
(Link)
> Что бы вы реализовали в первую очередь?

поспрашивай у немерлистов -- какие у них полезные макросы

а я как всегда выкачу обратный пример: система с 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)
(Reply) (Thread)
[User Picture]
From:xeno_by
Date:February 20th, 2013 08:06 am (UTC)
(Link)
пока что никак, надо думать. это была одна из причин, по которым я не хотел добавлять такую функциональность. но раз уж я придумал, как ее реализовать, то не смог устоять против того, чтобы попробовать.
(Reply) (Parent) (Thread)
From:M E
Date:February 20th, 2013 10:28 am (UTC)
(Link)
как *исследовательская* фича (помоделировать будущий язык) это весьма полезно, и может быть даже в продакшене в своем коде, но вот в продакшене в чужом библиотечном коде... бррр

в макросистеме плюсов (если что, я не о #define, а о шаблонах) макросы М1 и М2 можно определить, но не получится "одновременно" включить их оба в один файл, т.к. плюсы требуют предварительно объявлять класс, а у предварительно объявленного класса невозможно узнать наличие/отсутствие методов (и полей) -- т.е. М1 и М2 придется ручками упорядочить, причем первый из них не сможет работать
(Reply) (Parent) (Thread)
[User Picture]
From:xeno_by
Date:February 20th, 2013 11:09 pm (UTC)
(Link)
предлагаю еще пообщаться вот тут: http://xeno-by.livejournal.com/82242.html?thread=763458#t763458.
(Reply) (Parent) (Thread)
From:M E
Date:February 21st, 2013 03:48 am (UTC)
(Link)
я молчу, т.к. пока что не зарюхал че там одерски напридумывал насчет shift и continuations (но собираюсь)

по макросам по-моему достаточно уже высказал свою точку зрения -- я не буду участвовать в их развитии, но буду интересоваться, что получилось сделать с их помощью полезного; идеальным был бы чейнджлог типа "сделан такой-то макрос для такой-то задачи (1-2 абзаца про задачу и почему без макроса получалось плохо), подробности вот здесь"

еще интересен лог вида "вот такая-то задача требует неприятного boilerplate-а и возможно хорошо решиться макросами" (тоже 1-2 абзаца + кат или ссылка)

[не обязательно это форматировать именно в виде лога -- можно например в виде жж поста с 2 абзацами и разъяснением под катом; важно только чтобы можно было за этим следить, не углубляясь под кат и в комменты -- т.е. чтобы ты выносил полезные идеи из комментов в отдельные посты; возможно, ты захочешь это делать сразу по английски, но не знаю как тебе, а мне по-русски существенно комфортнее/легче писать]

[btw еще обрати внимание, насколько полезно вытаскивать коммент из середины в отдельный пост на примере "так ли нужна вся мощь макросов"]

мне лучше заняться изучением/обсуждением скалы (когда ты напишешь с какими тараканами приходится дружить, чем мешает перегрузка сигнатур и прочее)

еще мне интересно было бы изучить китайский придворный этикет метапрограммирование на языке имплицитов; думаю, тебе было бы полезно изучить поглубже этот вопрос, чтобы выкатывать use case-ы вида "вот на имплицитах это реализуется вот так, а на макросах вот так -- профит!"

тут видимо нужен некий cheat sheet -- как делать присваивание, цикл (или рекурсию), добавлять поля/методы к классу и т.п.

и вообще в целом я больше тут не писатель, а читатель

Edited at 2013-02-21 04:18 am (UTC)
(Reply) (Parent) (Thread)
[User Picture]
From:xeno_by
Date:February 21st, 2013 06:38 am (UTC)
(Link)
я запостил линк потому, что там есть примеры того, для чего может понадобиться инжекшн мемберов в класс и почему модели "макро аннотация может менять только аннотируемый дефинишен" недостаточно.
(Reply) (Parent) (Thread)
From:M E
Date:February 21st, 2013 09:51 am (UTC)
(Link)
а...

пока ты так не прокоментировал, мне было непонятно зачем смотреть

тогда да, если компилятор не давал вообще ничего инжектить -- это было плохо

что же касается обоих примеров -- первый, который я не понимаю и с невидимым объектом и второй, с atomic long -- то похоже инжектить ничего не надо

по 1 -- без невидимых объектов вроде как можно обойтись

по 2 -- вместо того чтобы инжектить рядом с лонгом этот лонг *заменяется* на структурку с геттером лонга и приватным для структурки AtomicLong

а *в общем случае* инжектить похоже да, придется -- скажем, когда захочется инжектить в статический компаньон из-за аннотации, подвешанной на нестатический метод или нестатическое поле

btw, а почему одерский сделал статические компаньоны? моя гипотеза -- чтобы они собирались сборщиком мусора, т.к. обычные статические объекты не собираются (в смысле, если больше не осталось объектов класса, и никто не указывает на экземпляр статического компаньона, состоящего из скажем небоксированных целых, то сборщик может его собрать, а в случае просто статического размещения сборщик собрать его не сможет)

Edited at 2013-02-21 10:06 am (UTC)
(Reply) (Parent) (Thread)
[User Picture]
From:xeno_by
Date:February 21st, 2013 10:51 am (UTC)
(Link)
Топ-левел компаньоны не собираются горбатым. Они компилируются в что-то вроде:
object C

class C$ {}
class C { static C$ MODULE$ = new C$ }
Соответственно, на единственный инстанс C$ будет указывать статический филд в классе C, т.е. он всегда будет достижим из gc roots.

Edited at 2013-02-21 10:52 am (UTC)
(Reply) (Parent) (Thread)
[User Picture]
From:xeno_by
Date:February 21st, 2013 10:51 am (UTC)
(Link)
Компаньоны сделаны, мне кажется, чтобы были first-class modules.
(Reply) (Parent) (Thread)
[User Picture]
From:xeno_by
Date:February 21st, 2013 06:40 am (UTC)
(Link)
"больше не писатель" потому, что я что-то не так сделал (проигнорировал какой-то фидбэк, пишу глупые вещи)? просто мне показалось, что раньше мы отлично беседовали, а теперь вот так вот получилось
(Reply) (Parent) (Thread)
From:M E
Date:February 21st, 2013 09:28 am (UTC)
(Link)
нда, я не заметил, что выражение двусмысленое получилось -- я вовсе не имел в виду что я не буду писать в твоем журнале, а имел в виду, что я в lj ("тут") больше читаю, чем пишу, а в данный краткий период времени я пишу необычно много, и стоило бы и почитать что-то в конце-то концов (т.к. я вроде высказался достаточно, чтобы моя точка зрения была понятна)

так что мы и сейчас отлично беседуем
(Reply) (Parent) (Thread)
[User Picture]
From:vit_r
Date:February 20th, 2013 06:39 am (UTC)
(Link)
Просто замечательная возможность для того, чтобы из разных частей программы объекты одного и того же класса пришли с двумя методами, называющимися одним именем, но делающих совершенно разное.
(Reply) (Thread)
From:M E
Date:February 20th, 2013 10:30 am (UTC)
(Link)
я че-то не разберу -- это такой сарказм, или действительно это бывает полезно?
(Reply) (Parent) (Thread)
[User Picture]
From:vit_r
Date:February 20th, 2013 10:34 am (UTC)
(Link)
я че-то не разберу

Мдя-с... :-/
(Reply) (Parent) (Thread)
From:M E
Date:February 20th, 2013 11:25 am (UTC)
(Link)
ниче подобного -- чтобы понять идею, надо знать собеседника

скажем, собеседник может иметь в виду например следующее:

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

это, с моей точки зрения, abuse, но при этом такая идея обсуждабельна

возможен еще более закрученный вариант, когда результат работы обоих методов не идентичен априори, но становится идентичным, если их вызывать только из правильного модуля (т.к. у каждого модуля свой инвариант, и методы заинлайнены руками под правильный инвариант)
(Reply) (Parent) (Thread)
[User Picture]
From:vit_r
Date:February 20th, 2013 11:31 am (UTC)
(Link)
Чтобы "понять собеседника" нужно всего лишь иметь немного опыта реальных промышленных проектов и общения с реальными программистами. В данном случае имеет место полное непонимание.

Edited at 2013-02-20 11:31 am (UTC)
(Reply) (Parent) (Thread)
From:M E
Date:February 20th, 2013 11:43 am (UTC)
(Link)
> В данном случае имеет место полное непонимание.

спасибо, к.о., именно с констатации моего непонимания я и начал

> Чтобы "понять собеседника" нужно всего лишь иметь немного опыта реальных промышленных проектов и общения с реальными программистами.

похоже тебе попадались слишком одинаковые "реальные программисты"
(Reply) (Parent) (Thread)
[User Picture]
From:vit_r
Date:February 20th, 2013 11:46 am (UTC)
(Link)
Ад будет в реальных проектах. Благо, язык уже дошёл до индусов.
(Reply) (Parent) (Thread)
From:M E
Date:February 20th, 2013 12:01 pm (UTC)
(Link)
слово "индус" все проясняет -- тогда да, я согласен с мыслью

жабка, кстати, отнюдь не в последнюю очередь выбилась вперед из-за того, что к ней можно подпускать индусов самым безопасным среди прочих языков образом
(Reply) (Parent) (Thread)
[User Picture]
From:vit_r
Date:February 20th, 2013 12:08 pm (UTC)
(Link)
Жаба выбилась потому, что Сан затратил на её рекламу безумные средства. В первых версиях это был тихий ужас, но менеджмент радостно прописывал её в самые серьёзные проекты. Большинство в результате с треском провалились.
(Reply) (Parent) (Thread)
From:M E
Date:February 20th, 2013 12:31 pm (UTC)
(Link)
когда сан загнулся и бегал продавался, я конкретно и со вкусом злорадствовал, т.к. меня эта реклама очень достала

жабка была тихим ужасом вплоть до 1.5, когда появились дженерики -- тогда она хотя бы с виду стала похожа на что-то приличное, хотя до сих пор она именно что только похожа
(Reply) (Parent) (Thread)
[User Picture]
From:vit_r
Date:February 20th, 2013 12:53 pm (UTC)
(Link)
В принципе, основные траблы для меня там были в multithreading и write once run anywhere.
(Reply) (Parent) (Thread)
From:M E
Date:February 20th, 2013 01:04 pm (UTC)
(Link)
теперь насчет "немного опыта реальных промышленных проектов" -- да, плюсы приучают не только к отсутствию акробатики, но даже к хождению строго по тропинке
(Reply) (Parent) (Thread)
[User Picture]
From:sorhed
Date:February 20th, 2013 08:33 am (UTC)
(Link)
А image-based persistence вроде smalltalk и некоторых лиспов с этим можно получить?
(Reply) (Thread)
[User Picture]
From:xeno_by
Date:February 20th, 2013 10:00 pm (UTC)
(Link)
Макросы не осилят, я думаю, переписывать байткод. Это совершенно новый уровень угара.
(Reply) (Parent) (Thread)
[User Picture]
From:diam_2003
Date:February 20th, 2013 08:42 am (UTC)
(Link)
Первое, что приходит в голову - какая-нибудь вариация на тему сериализации / десериализации.
(Reply) (Thread)
[User Picture]
From:theaspect
Date:February 20th, 2013 08:50 am (UTC)
(Link)
Во всяких джанго-петонах подобным образом строится орм.
(Reply) (Thread)
[User Picture]
From:thedeemon
Date:February 20th, 2013 09:12 am (UTC)
(Link)
+1, сразу вспомнился ActiveRecord в рельсах
(Reply) (Parent) (Thread)
[User Picture]
From:isorecursive
Date:February 20th, 2013 09:07 am (UTC)
(Link)
Ну я как-то упоминал, что хотел бы макроаннотациями для континуаций генерировать функции-компаньоны.
Если подробнее, для 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)
(Reply) (Thread)
[User Picture]
From:xeno_by
Date:February 20th, 2013 10:06 pm (UTC)
(Link)
Произвольные вклинивания я продемонстрировал потому, что доволен, что наконец-то получилось реализовать. Проблемы их, конечно, очевидны, но вдруг придет в голову какой-нибудь интересный юзкейс.

Касательно области действия аннотаций все не так просто. Вот уже в твоем примере @cpsParam совсем не ограничен аннотируемым ретурн тайпом метода, а должен иметь возможность влиять на контейнер функции.
(Reply) (Parent) (Thread)
[User Picture]
From:isorecursive
Date:February 20th, 2013 10:42 pm (UTC)
(Link)
> должен иметь возможность влиять на контейнер функции
Ну необязательно. Наверное, в моём случае правильнее будет генерировать f_pure в каком-нибудь вспомогательном сгенерированном объекте.

Вот одна из возможных простеньких и понятных стратегий:
Аннотации на методы - анализ тела метода, переписывание тела метода
аннотации на классы - анализ тела класса, переписывание тела класса
и т.д.
При этом можно разрешать произвольную кодогенерацию в каких-то невидимых объектах для внутреннего использования, интерфейс которых никак не будет затрагивать пользователя. Когда пользователь видит метод, но не имеет возможности найти, какой именно макрос из юнита компиляции его сгенерировал, это явно плохо. А вот когда метод используется внутреннее, пользователь его не будет использовать непосредственно, не будет знать о его существовании, и у него не возникнет надобности узнавать откуда он взялся - это вроде бы и нормально.
(Reply) (Parent) (Thread)
[User Picture]
From:xeno_by
Date:February 20th, 2013 11:07 pm (UTC)
(Link)
А куда запихивать такие невидимые объекты? Если их класть рядом (а их надо класть рядом, иначе они могут не увидеть какие-нибудь замыкаемые переменные из лексического скоупа), то все равно получается добавление мемберов по соседству.

Еще пример. Хотим мы реализовать аннотацию @atomic, которая обеспечивает атомарность апдейтов лонгов. Для обеспечения ее работы рядом с полем, помеченным аннотацией, нужно будет положить что-то вспомогательное, например, AtomicLong.
(Reply) (Parent) (Thread)
[User Picture]
From:isorecursive
Date:February 21st, 2013 11:38 am (UTC)
(Link)
> А куда запихивать такие невидимые объекты?
Можно в один и тот же private модуль внутри класса. В случае пожара, можно будет посмотреть всё вспомогательное, нагенерированное из аннотаций, отрефлексировав этот модуль.

Второй пример, если честно, не совсем понимаю, но можно сделать макроаннотацию на класс @WithAtomicGenerator("G"), которая генерит рядом со всеми полями, помеченными @apply("G"), какие-то AtomicLong. Вроде, так более-менее прозрачно всё даже для тех, кто не знает, как работает WithAtomicGenerator, но вкурсе правил на переписывание. Так ему будет понятно, что от связи аннотации на класс такой структуры с аннотациями на методы стоит ожидать появления новых полей.
(Reply) (Parent) (Thread)