?

Log in

No account? Create an account

Метапрограммирование в Агде и немного философии - Excelsior

Aug. 27th, 2013

09:49 am - Метапрограммирование в Агде и немного философии

Previous Entry Share Next Entry

Comments:

From:Valentin Budaev
Date:August 28th, 2013 11:14 am (UTC)
(Link)
Для этого надо понимать алгоритм работы гигиены в Racket. С каждым идентификатором проассоциирован набор syntax marks's. Два одноименных идентификатора могут быть связаны друг с другом тогда и только тогда, когда их marks не отличаются, то есть актуальный биндинг идентификатора - последний биндинг, появившийся до расхождения набора marks. Во время каждого раскрытия макроса генерируется своя уникальная mark и применяется рекурсивно к syntax object'у, потом этот object обрабатывается трансформером (собственно, логика макроса) и дальше к результату опять применяется та же mark. При этом если к объекту два раза подряд применяется одна и та же mark, то они взаимоуничтожаются. Выходит, что у идентификаторов, которые пришли макросу на вход, mark остается тот же, а к идентификаторам, сгенеренным макросом, добавляется mark макровызова. На примере, допустим у нас есть такой макрос:

(define-syntax-rule (macro arg) (let ([x 1]) (+ x arg)))

и мы его вызываем:
(let ([x 2]) (macro x))

(дальше включите Dr.Racket, напишите эти две формы и запустите макростеппер, активируйте stepper -> view syntax properies и stepper->extra options->one term at time, теперь все описанное будет зрительно видно - биндинги стрелочками при наведении на идентификатор, наборы марок - цветами, тыкнув на объект можно посмотреть все его свойства в окне справа)

сперва раскрывается само тело определения макроса и устанавливаются биндинги внутри него (и в квазицитате (let ([x 1]) (+ x arg)))). В этой точке уже связан + и let (т.о. фиксируется, что это + и let из стандартной библиотеки), а х остается свободной (это делается в раскрытии 1 фазы, то есть в степере уже будет сделано, т.к. стоит "hide phase > 0").

далее мы раскрываем (let ([x 2]) (macro x)) и устанавливается связывание для х=2 (то же сделано т.к. стоит "hide library syntax", можете поубирать эти hide галочки потом и посомтреть эти этапы более подробно). Потом раскрываем (macro x), сгенерируется марк с каким-то номером, этот марк применится рекурсивно к форме #'(macro x), соответственно, к идентификаторам #'x и #'macro (второе в дальнейшем важно!). Дальше #'x (уже с маркой и связыванием) подставляется на место arg и запускается макрос. Формируется форма (let ([x 1]) (+ x x)) и к ней опять рекурсивно применяется та же марка, результат возвращается из макроса. Т.о. у нас марка стоит на let, +, x в [x 1] и первом х в (+ x x), на втором х в (+ x x) марки нет. let и + были связаны до появления марок и все ок, так что теперь раскрывается сам этот let, х в let и первый х имеют одинаковые наборы марок - и связываются, второй х марок не имеет - по-этому не связывается, так что остается со своим связыванием во внешней let-форме.

Собственно, (syntax-local-introduce #'x) делает ни что иное, как применяет текущую марку к х, т.о. если х введен макросом, то он "выводит" х из-под гигиены (то есть х в итоге окажется без марки вовсе). (syntax->datum #'x 'y) предназначен для того, чтобы передать весь лексический контекст с х на y (и кроме того еще может передавать source location, syntax properties и вообще все свойства, какие есть у syntax object). Почему "не композируется" вариант с syntax->datum? У нас сперва раскрывается макрос aunless, который вешает на aif свою марку, потом начинает раскрываться aif и когда в нем делаем (syntax->datum #'aif 'it) то на it ложится марка с aif, которая там из aunless. А на it в форме макровызова (aunless 1 it it) никаких марок нет - в результате unbound variable.

> Также, есть ли где-то гайд на эту тему?

Только Racket Reference, в разделе syntax model, если не ошибаюсь. Больше не видел.

> Я этот сниппет с datum->syntax брал из паперы

Как я понимаю, везде суют этот datum->syntax, т.к. это классическое схемное решение (syntax-local-introduce в схеме нет, это чисто ракетовская фишка, связанная именно с ракетовской гигиеной, ну то есть конкретно с ее алгоритмом).
(Reply) (Parent) (Thread)
[User Picture]
From:xeno_by
Date:August 29th, 2013 07:25 am (UTC)
(Link)
>> и когда в нем делаем (syntax->datum #'aif 'it) то на it ложится марка с aif, которая там из aunless
Ооо!! Вот оно! Спасибо, теперь все стало понятно.
(Reply) (Parent) (Thread)
From:Valentin Budaev
Date:August 29th, 2013 09:28 am (UTC)
(Link)
Причем тут aif - это pattern-variable, то есть это именно aif, переданный в макроформу, а если бы у нас был паттерн (_ cond then else) вместо (aif cond then else), то это был бы тот aif, который само название макроса, тогда бы тоже не работало, но уже по другой причине - на it лежала бы марка от самого aif (т.к. идентификатор, в отличии от случая с pattern-variable, был бы сгенерен внутри макроса, а не получен в аргументе).
(Reply) (Parent) (Thread)
[User Picture]
From:xeno_by
Date:August 29th, 2013 07:29 am (UTC)
(Link)
>> В этой точке уже связан + и let (т.о. фиксируется, что это + и let из стандартной библиотеки), а х остается свободной
Можно подробнее на тему того, что в квазицитатах (как я понимаю, по сути, правая часть define-syntax-rule является квазицитатой) связывается, а что нет, а именно:
1) Как ракет понимает, что x связывать не надо?
2) Что было бы, если бы macro находился внутри лета, который объявляет свой собственный +? Плюс бы тогда связался или нет?
3) Что было бы, если бы в квазицитату что-то сплайсилось? Не знаю, как это выразить в ракете, поэтому напишу в Скале: q"val x = 1; $splicedTree; +(x, 1)". Здесь splicedTree может перекрыть биндинги как для икса, так и для плюса. Как это учтет ракет?
(Reply) (Parent) (Thread)
From:Valentin Budaev
Date:August 29th, 2013 09:24 am (UTC)
(Link)
> (как я понимаю, по сути, правая часть define-syntax-rule является квазицитатой)

Да, define-syntax-rule потом раскрывается в другую макроформу, где правая часть будет под #'

> 1) Как ракет понимает, что x связывать не надо?

Он не не понимает надо или не надо, просто он смотрит на квазицитату, видит там х и ищет переменную с соответствующим именем в лексическом окружении квазицитаты в -1 фазе (-1 для квазицитаты, поскольку квазицитата в макросе, то есть в фазе 1, то это будет окружение 0 фазы, то есть рантайма). let и + в этом окружении есть, а х - нету, вот х и остается свободной.

> 2) Что было бы, если бы macro находился внутри лета, который объявляет свой собственный +? Плюс бы тогда связался или нет?

Тогда бы + связался не с module-level + из стандартной библиотеки, а вот этим самым lexical +.

> Что было бы, если бы в квазицитату что-то сплайсилось?


(define-for-syntax q+ #'+)

(let ([+ 1])
(define-syntax (macro stx)
#`(#,q+ + +))
(macro))

в результате 2
то есть плюс из внешней квазицитаты остается внешним, внутренний - становится внутренним.

> десь splicedTree может перекрыть биндинги как для икса, так и для плюса.

Как он может их перекрыть?

Edited at 2013-08-29 09:30 am (UTC)
(Reply) (Parent) (Thread)
[User Picture]
From:xeno_by
Date:August 29th, 2013 09:33 am (UTC)
(Link)
2) То есть ракет бы разрешил такой квазицитате быть частью какого-то макро экспаншена только если этот макро экспаншен происходит под этим же самым летом?
(Reply) (Parent) (Thread)
From:Valentin Budaev
Date:August 29th, 2013 10:00 am (UTC)
(Link)
Вообще, выдернуть такую квазицитату вовне - это уже весьма нетривиальная задача:


(define-for-syntax x-var #f)

(define-syntax (set-var! stx)
(syntax-case stx ()
[(_ var) (begin (set! x-var (syntax-local-introduce #'var))
#'(void))]))

(define-syntax (get-var stx)
(syntax-local-introduce x-var))

(let ([x 1])
(set-var! x))

(let ([x 2])
(get-var))

ошибка: x: identifier used out of context in: x
при этом с марками все в порядке - то есть х который приходит из (get-var) марок не имеет.

И это логично вобщем-то - за пределами ее лексического контекста локальной переменной не существует.

Можно, вообще говоря, имея этот х собрать специально для него лет-форму, но зачем?

Edited at 2013-08-29 10:02 am (UTC)
(Reply) (Parent) (Thread)
[User Picture]
From:xeno_by
Date:August 29th, 2013 03:36 pm (UTC)
(Link)
Познавательный эксперимент, спасибо!

Я скорее имел ввиду вот это:
{
  val x = 2
  @forSyntax val qq = q"x"
  macro foo = qq
  foo
}
Как я понимаю, этот пример работать будет, так?

Edited at 2013-08-29 03:38 pm (UTC)
(Reply) (Parent) (Thread)
From:Valentin Budaev
Date:August 29th, 2013 04:25 pm (UTC)
(Link)
Будет, тут никакой проблемы нету. Контекст один - все, что внутри {}, ничего ниоткуда не выносится и все ок.

ЗЫ: как вставлять код, чтобы отступы не бились? А то лиспокод без отступов... ну... :)
(Reply) (Parent) (Thread)
[User Picture]
From:xeno_by
Date:August 29th, 2013 11:53 pm (UTC)
(Link)
Я использую
<code><pre>...</pre></code>


Edited at 2013-08-29 11:54 pm (UTC)
(Reply) (Parent) (Thread)
[User Picture]
From:xeno_by
Date:August 29th, 2013 09:34 am (UTC)
(Link)
val splicedTree = q"def +(x: Int, y: Int) = x * y"
q"val x = 1; $splicedTree; +(x, 1)"
(Reply) (Parent) (Thread)
From:Valentin Budaev
Date:August 29th, 2013 10:04 am (UTC)
(Link)
Ну сперва (при раскрытии _определения_ макроса, где цитата была) связывание + будет стандартное, со стандартным плюсом, а потом когда макрос раскроется, то анализируется раскрытый код и если с марками все ок, то + из сплайсинга перекроет старое связывание.

ЗЫ: я понял, к чему вы. с такой схемой типизировать цитаты принципиально невозможно - если только не добавлять в тип цитаты информацию о том, какие связывания она добавляет.

Edited at 2013-08-29 10:06 am (UTC)
(Reply) (Parent) (Thread)
[User Picture]
From:xeno_by
Date:August 29th, 2013 03:32 pm (UTC)
(Link)
Ага, понял. Мы пока что не замахиваемся на типизацию квазицитат, так что это не проблема.
(Reply) (Parent) (Thread)