LINUX.ORG.RU

Откуда у Scheme такие макросы?

 ,


1

2

Существует ли книга, в которой написано, почему макросы в scheme такие странные.

Логика макросов CL понятна. Всё есть список, дальше программист сам разберётся.

Часть решений в Scheme тоже: гигиена по-умолчанию, паттерны.

Но зачем нужна отдельная от списка структура кода (syntax)? Почему этот syntax, будучи созданным внутри макроса не привязывается автоматически к строке, где был использован макрос (вместо #' #` приходится писать syntax/loc)?

★★★★★

Но зачем нужна отдельная от списка структура кода (syntax)?

Потому что не всякий список — валидный код? Такие макросы, по сути, расширяют множество списков-которые-являются-валидным-кодом своими syntax-rules.

quasimoto ★★★★
()
Ответ на: комментарий от dave

Из-за Lisp-1?

И как lisp-1 заставляет различать списки и куски программы? Тот же picolisp является lisp-1, но макрос на вход берёт список.

monk ★★★★★
() автор топика
Ответ на: комментарий от quasimoto

Потому что не всякий список — валидный код?

Разве? Функция syntax, насколько знаю, принимает любой список на вход. Или приведи контрпример.

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

Да, я не про то заговорил. Из r6rs:

A syntax object is a representation of a Scheme form that contains contextual information about the form in addition to its structure. This contextual information is used by the expander to maintain lexical scoping and may also be used by an implementation to maintain source-object correlation

A syntax expression is similar to a quote expression except that (1) the values of pattern variables appearing within <template> are inserted into <template>, (2) contextual information associated both with the input and with the template is retained in the output to support lexical scoping, and (3) the value of a syntax expression is a syntax object.

То есть 1) lexical scoping и 2) source-object correlation.

Насчёт того к чему привязывается есть догадка — если мы используем (syntax <...>) в макросе, то создаётся _один_ синтаксический объект соответсвующий <...>, привязывается он к месту в макросе. Дальше, если использовать этот макрос, то этот объект просто сохраняет своё значение, иначе в каждом новом месте пришлось бы создавать новый объект перепривязывая его к новым локациям.

quasimoto ★★★★
()

Отдельная структура нужна, чтобы тащить source location. В говносписках для столь важной информации места не предусмотренно.

anonymous
()
Ответ на: комментарий от quasimoto

http://stackoverflow.com/q/10862665/1337941

При этом

(syntax-rules (literal-id ...)
  [(id . pattern) template] ...)
Equivalent to

(lambda (stx)
  (syntax-case stx (literal-id ...)
    [(generated-id . pattern) (syntax-protect #'template)]  ...))

(c) http://docs.racket-lang.org/reference/stx-patterns.html#(form._((lib._racket/...

Хотя должно быть тогда

(lambda (stx)
  (syntax-case stx (literal-id ...)
    [(generated-id . pattern) (syntax-protect (syntax/loc stx template))]  ...))

monk ★★★★★
() автор топика
Ответ на: комментарий от anonymous

Отдельная структура нужна, чтобы тащить source location.

А зачем его тащить, если source location всё равно однозначно определяется местом вызова макроса? В смысле, внутри (syntax-case stx ...) можно указать какое-то другое осмысленное место кроме stx?

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

Нехорошо получается. Это как если бы ошибки в использовании loop или defpackage посылали читать сорсы loop и defpackage.

quasimoto ★★★★
()
Ответ на: комментарий от quasimoto

Нехорошо получается.

Вот именно. Фактически, по моему мнению, первый аргумент syntax-case внутри него должен быть параметром по-умолчанию для всех форм syntax. Иначе такие «ошибки по недосмотру» всё равно будут периодически вылазить.

Про гигиену тоже вопрос: чем пользоваться для отладки? (syntax->datum (expand-once ...)) не показвает, какие идентификаторы запорчены гигиеной.

(define-syntax-rule (swap x y)
  (let ([tmp x])
    (set! x y)
    (set! y tmp)))

> (syntax->datum (expand-once '(swap x tmp)))
'(let ((tmp x)) (set! x tmp) (set! tmp tmp))

Для сравнения CL

(defmacro swap (x y) 
  (let ((tmp (gensym "TMP"))) 
    `(let ((,tmp ,x)) 
       (setf ,x ,y) 
       (setf ,y ,tmp))))

> (macroexpand-1 '(swap x tmp))
(LET ((#:TMP4964 X)) (SETF X TMP) (SETF TMP #:TMP4964))

Здесь чётко видно, что пришло из параметров, а что сгенерировалось макросом.

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

Хотя должно быть тогда

В исходниках так и есть:

[(_ . pattern) (syntax-protect (syntax/loc user-stx template))]
Видимо в документации ошибка, или упростили (хотя в примерах из документации везде #', а это, действительно, в заблуждение вводит).

qaqa ★★
()
Ответ на: комментарий от qaqa

В общем, у CL есть gensym, а у Scheme — syntax/loc. И то и то понятно где и как использовать, но легко забыть и последствия вылезут неизвестно когда.

monk ★★★★★
() автор топика
Последнее исправление: monk (всего исправлений: 1)
Ответ на: комментарий от monk

Если уж очень хочется:

(define-syntax (swap stx)
  (syntax-case stx ()
    [(_ x y)
     (let ([tmp (gensym 'tmp)])
       (quasisyntax/loc stx
         (let ([#,tmp x])
           (set! x y)
           (set! y #,tmp))))]))

> (syntax->datum (expand-once '(swap x y)))
'(let ((tmp281059 x)) (set! x y) (set! y tmp281059))

qaqa ★★
()
Ответ на: комментарий от monk

Есть подозрение, что отсустствие syntax/loc в примерах из документации, и вообще лаконичность документации по этому вопросу - один из висяков, на которые тупо у разработчиков нет времени. Не хватает им народу, чтобы рутиной заниматься. Так, что если есть время - вливайся :)

qaqa ★★
()
Ответ на: комментарий от monk

Про гигиену тоже вопрос: чем пользоваться для отладки?

В принципе (http://www.ccs.neu.edu/racket/pubs/cf-sp09.pdf) — expand/step-text показывает:

> (expand/step-text #'(swap x tmp))
Macro transformation
(swap x tmp)
  ==>
(let:1 ((tmp:1 x)) (set!:1 x tmp) (set!:1 tmp tmp:1))

Macro transformation
(let:1 ((tmp:1 x)) (set!:1 x tmp) (set!:1 tmp tmp:1))
  ==>
(let-values:2 (((tmp:1) x)) (set!:1 x tmp) (set!:1 tmp tmp:1))

Tag top-level variable
(let-values:2 (((tmp:1) x)) (set!:1 x tmp) (set!:1 tmp tmp:1))
  ==>
(let-values:2 (((tmp:1) (#%top . x))) (set!:1 x tmp) (set!:1 tmp tmp:1))

Tag top-level variable
(let-values:2 (((tmp:1) x)) (set!:1 x tmp) (set!:1 tmp tmp:1))
  ==>
(let-values:2 (((tmp:1) x)) (set!:1 x (#%top . tmp)) (set!:1 tmp tmp:1))

как и Stepper -> Extra options -> Always suffix marked identifiers. Правда оно не даёт syntax object с marked identifiers (?).

quasimoto ★★★★
()
Ответ на: комментарий от qaqa

Если уж очень хочется

:-)))

Нет, так не хочется.

Я имел в виду, что нужен какой-то инструмент, позволяющий увидеть разные биндинги чем-то отличающимися.

Для простого случая, конечно можно просто пометить все параметры

> (syntax->datum (expand '(swap :x :y)))
'(let-values (((tmp) :x)) (set! :x :y) (set! :y tmp))

Но вот пример: http://docs.racket-lang.org/guide/pattern-macros.html#(part._pattern-macro-ex...

(define-for-cbr do-f (a b)
  () (swap a b))
=> (define-for-cbr do-f (b)
     ([a get_1 put_1]) (swap a b))
=> (define-for-cbr do-f ()
     ([a get_1 put_1] [b get_2 put_2]) (swap a b))
=> (define (do-f get_1 get_2 put_1 put_2)
     (define-get/put-id a get_1 put_1)
     (define-get/put-id b get_2 put_2)
     (swap a b))

Спасибо автору документации, что расставил вручную _1, _2. А если просто распаковывать через expand-once, то получим

(define-for-cbr do-f (a b)
  () (swap a b))
=> ... =>
(define (do-f get get put put)
     (define-get/put-id a get put)
     (define-get/put-id b get put)
     (swap a b))

Может есть какой-нибудь инструмент позволяющий проследить именно биндинги, а не символы (раз уж в Scheme символ может быть настолько многозначным)?

monk ★★★★★
() автор топика
Ответ на: комментарий от qaqa

Не хватает им народу, чтобы рутиной заниматься. Так, что если есть время - вливайся :)

Сначала G-ObjectIntrospection-FFI доваяю. И чтобы писать документацию надо знать ответ на вопрос «зачем?». Поэтому и прошу какую-нибудь книгу, где описаны причины (Rationale) всех этих наворотов в Scheme.

Пример «безумного наворота»:

(let ([x 1])
    (displayln x)
    (define y (+ x 1))
    (case y
      [(1) 2]
      [(2) 3]
      [else 4])
    (begin
      (define x 2))
    y)
. . +: contract violation
  expected: number?
  given: #<undefined>
  argument position: 1st

Очень неочевидно.

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

Да, были у меня такие приколюхи, когда стал повсеместно define вместо let использовать для внутренних определений. Тут надо аккуратнее повторно идентификаторы использовать. Ну или let везде.

qaqa ★★
()
Ответ на: комментарий от qaqa

Ну или let везде.

Не рекомендуется: http://www.ccs.neu.edu/home/matthias/Style/style/Choosing_the_Right_Construct...

На самом деле ничего страшного в этом нет, привыкнуть можно. Неясно одно: если define влияет на начало блока, то почему он не устанавливает там значение?

Вот такое работает

(let ()
    (set! y 1)
    (define x (+ 1 y))
    (define y 2)
    (define x2 (+ 1 y))
    (cons x x2))

'(2 . 3)

Получается, что define определяет биндинг на весь блок, а значение только на участок после команды. Опять же вопрос: почему сделано именно так, для чего?

monk ★★★★★
() автор топика
Последнее исправление: monk (всего исправлений: 1)
Ответ на: комментарий от monk

У внутреннего define семантика letrec, это надо помнить (см.Reference, 1.2.3.7 Internal Definitions).

(let-values ()
  (letrec-values ([() (begin (set! y (quote 1)) (#%app values))]
                  [(x) (#%app + (quote 1) y)]
                  [(y) (quote 2)])
    (let-values ([(x2) (#%app + (quote 1) y)])
      (#%app cons x x2))))

qaqa ★★
()
Ответ на: комментарий от qaqa

Тогда объясните, почему должно не работать:

(let ([x 1])
    (letrec ([y (+ x 1)]
             [x 2])
      y))
+: contract violation
  expected: number?

???

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

В смысле, понятно, что по документации.

Но зачем «but the locations for all ids are created first and filled with #<undefined>»?

monk ★★★★★
() автор топика
Ответ на: комментарий от qaqa

Вот нашел в логах #racket по этому поводу

...

03:44 paddymahoney: does anyone have experience modifying the way Racket partially expands internal definition context?

13:31 stamourv: paddymahoney: What are you tring to do?

14:48 paddymahoney: stamourv: so I'd like to change the rule for expanding defines, so that only the context following the definition is enriched with the bindings defined. Right now, I'm getting confused because statements above a define are referring to those bindings instead of ones bound by an enclosing lambda

14:49 paddymahoney: would a snippet help? one sec.

14:52 paddymahoney: stamourv: http://pastebin.com/rguwD7af

14:54 paddymahoney: stamourv: line 27 and 28. This came up when I was working with Kaylin from this channel on their irc code

15:00 stamourv: paddymahoney: Right, I see what you mean.

15:02 stamourv: For practical purposes, I'd recommend using `let*' in cases like that.

15:02 paddymahoney: stamourv: yeah, it has been bugging me, cause it rears its head using internal defines in many functions.

15:03 stamourv: If you want to experiment with changing the behavior of internal definitions contexts, I'm sure you can't do that, but I don't have a good way off the top of my head.

15:03 stamourv: In general, though, that behavior of internal definition contexts is desirable. You definitely want `letrec*', not `let*'. It's unfortunate that it sometimes causes confusion.

15:03 paddymahoney: stamourv: thanks! I'll do a dig at some point.

20:15 paddymahoney: stamourv: I think what I might shoot for is moving to let* in internal definition contexts, and allow a form rec that creates an internal definition context with letrec* (with the current semantics for internal definition context.

qaqa ★★
()
Ответ на: Вот нашел в логах #racket по этому поводу от qaqa

Мда... Ладно, фиг с ним с Racket. Но хоть по R6RS должна быть книга или хоть протокол принятия этого стандарта... хоть что-нибудь.

После CL в scheme ощущение такое, словно не понимаю каких-то основ, которые очевидны всем разработчикам scheme. Как если программировать на Haskell просто посмотрев do-нотацию и не читая про монады вообще — программировать можно, но многие вещи неочевидны.

monk ★★★★★
() автор топика
Ответ на: комментарий от qaqa

А как по другому, если любой id может быть использован в любом val-expr?

Как в CL. labels позволяет определять только тело функций, а не значения. В смысле, было бы

(letrec ([is-even? (n)
             (or (zero? n)
                  (is-odd? (sub1 n))))]
         [is-odd? (n)
             (and (not (zero? n))
                  (is-even? (sub1 n))))])
    (is-odd? 11))

и никаких промежуточных состояний, когда переменная уже есть, а значения ещё нет уже быть не может.

monk ★★★★★
() автор топика
Ответ на: комментарий от qaqa

http://www.r6rs.org/final/html/r6rs/r6rs.html

Это ответ на вопрос «как должно быть?». А мне нужен ответ на вопрос «почему так?».

Также как ANSI CL standard не отвечает на вопрос «почему функции elt и nth получают аргументы в разном порядке», а вот история создания стандарта — отвечает.

monk ★★★★★
() автор топика
Ответ на: комментарий от qaqa

http://www.scheme.com/tspl4/

Спасибо! По крайней мере кусок про define явно объяснён. Про syntax-case и syntax пока не нашёл.

monk ★★★★★
() автор топика
Ответ на: комментарий от qaqa

http://www.scheme.com/tspl4/

Ещё раз спасибо! Про синтаксические объекты тоже ясно описано. source location там просто надстройка сбоку. А реально надо именно для того, чтобы различать внутри макроса разные биндинги к одному символу. В общем, без синтаксических объектов гигиену на порядок сложнее сделать (если вообще возможно).

monk ★★★★★
() автор топика

Но зачем нужна отдельная от списка структура кода (syntax)?

Это уже речь о racket а не о схеме. Логика проста - из-за гигиены вы не можете использовать plain-символы, т.к. нужен враппер с информацией о связывании. Но если испольуем враппер для символов, почему бы н использовать аналогичный для списков, раз это безхо всякого усложнения по сравнению с макросистемой CL даст кучу профитов?

Почему этот syntax, будучи созданным внутри макроса не привязывается автоматически к строке, где был использован макрос (вместо #' #` приходится писать syntax/loc)?

Он привязывается к той строке, в которой, собственно, и определена форма. То есть если вы написали #'(+ x y) в строке 10, то у нее и будет location со строкой 10. Очевидно что такое поведение нужно чуть менее чем всгда так - что понятно, почяему |#' устанавливает в качестве location место использования. Почему же макросы типа syntax-rules не обрамляют автоматически код в syntax/loc вместо syntax - эт уже другой вопрос. Гипотезы, почему таК, у меня две:

1. Разработчики решили, что если ге-то есть нестандартне поведение - то пусть пользователи указывают это ЯВНО.

2. Чтобы макросы в отладчике вели себя как обычные ф-и. Например, если написать (define-syntax-rule (bla-bla x y) (foo x y)), (define-syntax-rule (foo x y) (+ x y)), то при текущем поведении в отладчике при пошаговом исполнении формы (bla-bla 1 2) будет все как при исполнении ф-й прыгать сперва на (foo x y), потом на (+ x y), ели поменять поведение, то курсор как был на (bla-bla 1 2) так с нее никуда и не уйдет при дальнейшем выполнении.

anonymous
()
Ответ на: комментарий от monk

Функция syntax, насколько знаю, принимает любой список на вход.

syntax - это не функция. У обычного списка нету той информации, которую содержит в себе снтаксический объект. Использование этой информации позволяет писать макросы, которые трудно или вовсе невозможно без фундаменталього переписывания макросистемы написать в CL. В этом и причина.

anonymous
()

Вся суть боли динамического петушения ITT.

anonymous
()
Ответ на: комментарий от monk

А зачем его тащить, если source location всё равно однозначно определяется местом вызова макроса? В смысле, внутри (syntax-case stx ...) можно указать какое-то другое осмысленное место кроме stx?

Во-первых, там не только source location, во-вторых - нет, не ондозначно определяется, в-третьих - да, в 99% случаев там НЕ stx должно быть. stx только у ВНЕШНЕЙ фрмы, но если вы аписали (define-syntax-rule (foo x y) (+ x y)) кроме позиций #'(+ x y) есть еще позиции у #'a, #'x, #'y. Позиция которых - НЕ stx, например у x и y она будет соответстввать позициисоответствующих форм в макровызове (foo 1 2) - #'1 и #'2 соответственно.

anonymous
()
Ответ на: комментарий от anonymous

Ура! Появился тот самый анунимус!

Зачем сделано так:

(let ([else #f])
  (cond [else (write "oops")])) 

does not write "oops", since else is bound lexically.

?

Почему не сравнивать по eq?

P.S. Может зарегистрируешься? Хоть скастовать можно будет...

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

В общем, у CL есть gensym, а у Scheme — syntax/loc. И то и то понятно где и как использовать, но легко забыть и последствия вылезут неизвестно когда.

Но только в схеме генсим тоже есть (точнее не нужен изза гигиены). Кроме того неиспользование генсима приводит к ошибке, а не использование syntax/loc - не приводит (оно приводит к тому что ошибка не позиционируется точно). Ну и в случае CL syntax/loс нету вовсе, по-этому там позиционировании будет как в схеме, но только без возможности исправления.

anonymous
()
Ответ на: комментарий от monk

Вот именно. Фактически, по моему мнению, первый аргумент syntax-case внутри него должен быть параметром по-умолчанию для всех форм syntax.

Нет, не должен. В большей части случаев (99%) надо захватывать текущее положение, а не stx, вот оно и сделано так по умолчанию.

Про гигиену тоже вопрос: чем пользоваться для отладки? (syntax->datum (expand-once ...)) не показвает, какие идентификаторы запорчены гигиеной.

макростеппером, конечно. Ничего лучше еще не придумали. Как можно отлаживать макросы руками через одиночные экспанды - вообще не представляю.

anonymous
()
Ответ на: комментарий от monk

Может есть какой-нибудь инструмент позволяющий проследить именно биндинги, а не символы (раз уж в Scheme символ может быть настолько многозначным)?

Макростеппер подсвечивает биндинги стрелочки, и выделяет марки цветами или подписыванием номеров. еще там можно сразу посмотреть все свойства объекта/символа.

anonymous
()
Ответ на: комментарий от monk
(define-syntax cond2
  (lambda (x)
    (syntax-case x ()
      [(_ (e0 e1 e2 ...))
       (eq? (syntax->datum #'e0) 'else)
       #'(begin e1 e2 ...)]
      [(_ (e0 e1 e2 ...)) #'(if e0 (begin e1 e2 ...))]
      [(_ (e0 e1 e2 ...) c1 c2 ...)
       #'(if e0 (begin e1 e2 ...) (cond c1 c2 ...))]))) 

(let ([else #f])
  (cond2 [else (write "oops")])) 
"oops"

Так работает. В чём фатальный недостаток?

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

Когда опредеделяется ф-я, то переменная уже должна быть. Но ее значение вполне возможно не определено. Тогда возникает вопрос - а что будет в этом значении, если мы все-таки к нему как-то обратимся? По сути что угодно и будет в случае ощибки непонятно вообще, что происходит. Вот в схеме и решили - пусть будет не что угодно, а специальное значение undefined, которое и будет указывать на характер ошибки.

anonymous
()
Ответ на: комментарий от monk

Хоть скастовать можно будет...

Я и так все треды по racket регулярно смотрю, просто последние дни я не дома и у меня нету нормального доступа к компьютеру. Скастовать бы в любом случае не удалось.

Почему не сравнивать по eq?

Почитай в блоге racket статью про syntax parameters, там эта тема в том числе поднимается. Если будет неясно тогда уже попытаюсь как-то подробнее объяснить.

anonymous
()
Ответ на: комментарий от monk

В том, что мы можем намеренно связать else в своем коде и использовать его внутри cond как обычное значение.

Макросистема должна respect lexical structure (то есть если определили в блоке переменную, то она везде в блоке должна иметь соответствующее значение, если только явно не переопределена в внутри), а с твоим вариантом кода она эту структуру нарушает.

anonymous
()
Ответ на: комментарий от anonymous

через (expand/step form) можно вызвать окно макростеппера для разбора одной формы form (даже в репле если dr.racket не включен), не обязательно включать степпер для всего кода кнопкой expand в dr.racket.

anonymous
()
Ответ на: комментарий от anonymous

Почитай в блоге racket статью про syntax parameters

Если ту, что про анафорические макросы и переменную it, то читал. Но здесь ведь не та ситуация. Нам не надо значение else. Мы всего лишь помечаем ветку по-умолчанию.

Макросистема должна respect lexical structure (то есть если определили в блоке переменную, то она везде в блоке должна иметь соответствующее значение, если только явно не переопределена в внутри).

(define-syntax cond2
  (lambda (x)
    (syntax-case x ()
      [(_ (e0 e1 e2 ...))
       (eq? (syntax->datum #'e0) 'else)
       #'(begin e1 e2 ...)]
      [(_ (e0 e1 e2 ...)) #'(if e0 (begin e1 e2 ...) #f)]
      [(_ (e0 e1 e2 ...) c1 c2 ...)
       #'(if e0 (begin e1 e2 ...) (cond2 c1 c2 ...))]))) 

(let ([else #f])
    (cond2 [1 else])) => #f

(let ([else #f])
    (cond2 [else else] [else 1])) => 1

Значение везде используется корректно.

«The last <cond clause> may be an “else clause”, which has the form

(else <expression1> <expression2> ...).» (с) http://www.r6rs.org/final/html/r6rs/r6rs-Z-H-14.html#node_sec_11.4.5

Разве это не подразумевает, что при соответствующей форме (else ...) значение переменной else не должно проверяться?

P.S. Зачем там вообще else, если достаточно #t ?

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

(let ([else #f]) (cond [else 1])) должно быть #f а у тебя 1. И вообще это общая практика - если в твоем макросе есть кейворд (как в cond) то некоторый другой макрос может раскрыться в твой. И пользователь этого другого макроса может использовать твой кейворд как обычный иденьтификатор и всунет его в этот макрос. В результате все скорее всего сломается. Поэтому по умолчанию надо делать literaly а не eq?.

anonymous
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.