?

Log in

No account? Create an account

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

Feb. 19th, 2013

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

Previous Entry 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: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) (Expand)
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) (Expand)
[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) (Expand)
[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) (Expand)