LINUX.ORG.RU

Вопрос по макросам

 


1

2

До меня не доходит до конца смысл макросов. Очевидно, что, все выражения, которые можно раскрыть в компилтайме, можно раскрыть и в рантайме. Если экспандер написан на лиспе, например, скорость макроэкспанда будет одинаковой. То есть, мы тут ничего не выигрываем в плане производительности. С другой стороны, необходимость раскрытия той или иной формы может быть продиктована в самом рантайме, таким образом, часть форм раскрывать, возможно, не придется, тогда мы экономим ресурсы. Ведь в компилтайме раскрываются все макросы, не так ли? Тогда какое преимущество мы получаем от раскрытия в компилтайме?



Последнее исправление: terminator-101 (всего исправлений: 2)

Если экспандер написан на лиспе, например, скорость макроэкспанда будет одинаковой.

Но если экпанд был при компиляции, то в рантайме ничего уже экспандить не надо. По-моему очевидно.

Тогда какое преимущество мы получаем от раскрытия в компилтайме?

Ты опять хочешь потроллить на нему fexp'ов?

no-such-file ★★★★★
()
Ответ на: комментарий от no-such-file

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

terminator-101
() автор топика
Ответ на: комментарий от terminator-101

один раз скомпилировал, дальше работает маш код.

Вообще говоря, так и есть. Перекомпиляция только при изменении функции. На Си тоже так можно, но надо каждую функцию в отдельный .so упаковать. И макросы можно. Как обработку исходного кода перед компиляцией :-)

monk ★★★★★
()

Очевидно, что, все выражения, которые можно раскрыть в компилтайме, можно раскрыть и в рантайме.

Ещё один нюанс: раскрытие внутри цикла. Если есть цикл на 1000 итераций, а внутри макрос (fexpr), то в рантайме придётся этот участок 1000 раз перекомпилировать.

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

Ну, это понятно, но если раскрытие в цикле не нужно, программист ясное дело вынесет его за цикл. Это с любым выражением то же самое.

while(condition) 1+1+a
 vs
const=1+1 
while(condition) const+a
Это из той же оперы.

terminator-101
() автор топика
Ответ на: комментарий от terminator-101

если раскрытие в цикле не нужно, программист ясное дело вынесет его за цикл

Макрос обычно нужен ради побочных эффектов, а не результата.

(dotimes (n 1000)
  (assert (check arr n) "Bad value at ~a" n)
  (process arr n))

assert раскрывается в проверку, если *debug* = t и в ничего в противном случае (полный аналог Сишного assert). Но если бы это бы fexpr, то он даже при *debug* != t жутко тормозил бы систему, так как запуск компилятора дороже, чем (process arr n), а обойти его нельзя.

monk ★★★★★
()

Макросы не нужны. :) А вообще, у Пола Грэма есть отдельная глава про то зачем нужно использовать макросы и когда. Раскрываются они в компилетайме. Я тут рассматриваю современные компиляторы. Как с несовременными не в курсе. Т.е. макрос:

(defmacro myincf (var increment)
  `(setf ,var (+ ,var ,increment)))

В пользовательском коде:

(loop for i from 0 to 5 do (myincf a 2))

Будет раскрыт сначала в форму:

(loop for i from 0 to 5 do (setf a (+ a 2)))

А затем скомпилирован.И в цикле оно не будет раскрываться каждый раз. Потому что макрос раскрывает форму. Другое дело, если ты напишешь индусский код, где ты сам будешь делать macroexpand. В примере для простоты я не раскрыл макрос loop, который тоже на самом деле раскроется в неведомую хрень. :)

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

если макрос раскриваетса в компаилтайм, то как работает это:

(let ((op 'car)) (defmacro pseudocar (lst) `(,op ,lst)))

(macroexpand-1 '(pseudocar (a b))) (CAR (A B))

op должна получится значение во время исполнение. что за магия?

anonymous
()

Если экспандер написан на лиспе, например, скорость макроэкспанда будет одинаковой. То есть, мы тут ничего не выигрываем в плане производительности.

Что значит, не выигрываем? Нам не приходится раскрывать макросы рантайме, отсюда и выигрыш.

Вообще макросы - это компилируемые фекспры. То есть язык с фекспрами компилировать нельзя, с макросами - можно. Соответственно у макросов те же преимущества, что и у компиляции.

anonymous
()

Ну и еще один момент - если в макросах есть какие-то сайдэффекты, то они гирантированно будут исполнены до начала исполнения нашего кода. Если же у нас макросы исполняются одновременно с кодом - это будет засирать контролфлоу. В частности, пример теми же макросами в цикле - если макрос раскрывается в рантайме, то все его эффекты будут исполнены на каждую итерацию цикла, что нарушит семантику. Или другой пример - если макрос раскрывается в одной из if веток а контрол-флоу идет в другую, то макрос не будет раскрыт вовсе и его сайдэффекты не будут исполнены. То есть раскрытие макросов в рантайме нарушает code-as-data.

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

Другое дело, если бы это было из функции, где ты делаешь дефмакро. Но это, скажем так, неканоничный подход. :) Зачем так делать то?

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

Если попытаться скомпилировать - не работает. И если засунуть вызов макроса тот же let внутри которого он определяется - тоже не работает.

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

Если попытаться скомпилировать - не работает.

Зато через load работает :-)

А чтобы работало через compile-file надо делать

(eval-when (:compile-toplevel :load-toplevel :execute)
  (let ((op 'car)) (defmacro pseudocar (lst) `(,op ,@lst))))
 
(format t "~a" (macroexpand-1 '(pseudocar (a b))))

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

Но раскрыть и объявить макрос внутри одного let будет уже нельзя, если макрос использует связанные в let биндинги :)

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

Можно:

(eval-when (:compile-toplevel :load-toplevel :execute)
  (let ((op 'car))
     (defmacro pseudocar (lst) `(,op ,@lst))
     (format t "~a" (macroexpand-1 '(pseudocar (a b))))))

Компилируется и работает.

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

Так у тебя format в eval-when, убери его оттуда.

У меня let в eval-when. А ты сказал, что хочешь видеть format внутри let.

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

алсо:

(eval-when (:compile-toplevel :load-toplevel :execute)
  (let ((op 'car))
     (defmacro pseudocar (lst) `(,op ,@lst))
     (pseudocar (a b))))
уже не работает

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

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

У меня let в eval-when. А ты сказал, что хочешь видеть format внутри let.

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

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

уже не работает

Работает. Проверил sbcl и clisp'ом


* (compile-file "let-macro")

; compiling file "/home/monk/languages/lisp/test/let-macro.lisp" (written 04 NOV 2014 11:13:20 PM):
; compiling (LET (#) ...)
; file: /home/monk/languages/lisp/test/let-macro.lisp
; in: EVAL-WHEN (:COMPILE-TOPLEVEL :LOAD-TOPLEVEL :EXECUTE)
;     (PSEUDOCAR (A B))
; ==>
;   (CAR A B)
;
; caught WARNING:
;   The function was called with two arguments, but wants exactly one.

debugger invoked on a UNBOUND-VARIABLE in thread
#<THREAD "main thread" RUNNING {ACD37A1}>:
  The variable A is unbound.

В смысле, макрос раскрывается, а дальше выражение (car a b) смысла не имеет.

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

раскрытие макроса должно быть обычным раскрытием, не в at-compile.

Тогда оно не может быть в том же let. Потому что (let (defmacro ...)) обязательно должно быть в eval-when.

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

Нет. Ты сказал «раскрыть и объявить макрос внутри одного let будет уже нельзя, если макрос использует связанные в let биндинги». Когда я показал, что можно, ты сказал, что надо написать так, чтобы раскрытие макроса не было в этом let. Так чего ты хочешь?

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

Ты сказал «раскрыть и объявить макрос внутри одного let будет уже нельзя, если макрос использует связанные в let биндинги».

Да.

ты сказал, что надо написать так, чтобы раскрытие макроса не было в этом let.

Нет, оно должно быть в этом let, но не должно раскрываться at-compile.

Вообще изначально речь шла о том, что биндинг в let должен связываться не в at-compile и вопрос был почему в cl это работает (хотя оно не работает).

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

биндинг в let должен связываться не в at-compile

Тогда при компиляции макрос работать не будет. Так как в at-compile есть закрытый список toplevel форм, которые выполняются. let (и всё, что внутри него) не выполняется.

почему в cl это работает

Так написан стандарт. Я в Racket первое время всё напарывался на то, что (let ([a #f]) (define (foo ...) ...) (define (bar ...) ...) (void)) не определяет функции. Ибо внутри let они локальные.

А в CL порядок компиляции описан вполне строго в стандарте. Он позволяет делать всякие извраты с eval-when (например, поставить :compile без всего остального — при компиляции работает, при загрузке из исходников — нет), но в любом случае достаточно детерминирован. И если где надо втыкать (eval-when (:compile-toplevel :load-toplevel :execute), то даже одинаково себя ведёт при любом режиме компиляции/загрузки/запуска программы.

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

Тогда при компиляции макрос работать не будет.

Ну да, с того и начали :)

А в CL порядок компиляции описан вполне строго в стандарте. Он позволяет делать всякие извраты с eval-when

Фишка в том, что при нормальной семантике эти извраты не нужны.

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

при нормальной семантике

«нормальной» == «как в Scheme»? Я уже по этому поводу целый тред создавал: Анализ пользователей Common Lisp и Racket

Для той цели, для которой используется Common Lisp (разработка в образе, отладка в REPL'е, главное — удобство процесса) его семантика вполне адекватна.

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

эти извраты не нужны

Если намёк на Racket, то необходимость писать (module m ...) (reqiure 'm (for-syntax 'm)) вместо eval-when изврат ещё больший.

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

то необходимость писать (module m ...) (reqiure 'm (for-syntax 'm)) вместо eval-when изврат ещё больший.

Так какая разница между (eval-when (load ...)) и (require (for-syntax ,,,))? Разница тут в том, что в racket состояние в фазах разделено и потому все автоматически разруливается, что для CL неверно - надо следить за консистентностью состояния «руками».

Отсюда и требования более «извратных» средств.

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

Для той цели, для которой используется Common Lisp (разработка в образе, отладка в REPL'е, главное — удобство процесса) его семантика вполне адекватна.

То, что все может сломаться (причем весьма неожиданным и незаметным способом) если не сделать (не знать, забыть, ошибиться) как и где нужно eval-when - в этом ничего адекватного нет. Тем более, что взамен гарантий, что все будет хорошо, мы ничего не теряем.

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

То, что все может сломаться (причем весьма неожиданным и незаметным способом) если не сделать (не знать, забыть, ошибиться) как и где нужно eval-when

Если делать рекомендуемым способом через asdf, то забыть eval-when не проще, чем забыть require for-syntax.

А если нет, то и в Racket через load семантика не такая, как через require.

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

А если нет, то и в Racket через load семантика не такая, как через require.

Такая же. В ракет в transformer environment только то, что в begin-for-syntax

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

Такая же.

$ cat test.rkt
#lang racket
(define x 6)
(displayln (eval 'x))

$ racket test.rkt
x: unbound identifier;
 also, no #%top syntax transformer is bound
  in: x
  context...:
   /home/monk/test/test.rkt: [running body]

$ cat test2.rkt
(define x 6)
(displayln (eval 'x))

$ racket -f test2.rkt
6
monk ★★★★★
()
Ответ на: комментарий от monk

У разного кода - разная семантика, все верно. А если загружать один и тот же код - то одинаковая.

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

А если загружать один и тот же код - то одинаковая.

Код отличается только методом загрузки. В одном случае #lang racket, в другом — load.

Также и в CL. В одном файле 3 частично совпадающих кода: стадии компиляции, стадии загрузки и стадии выполнения.

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

Код отличается только методом загрузки.

Нет.

В одном случае #lang racket

#lang racket - это не метод загрузки, это враппер (module 'anonymous racket code ...). Загрузка же в обоих случаях наоборот совершенно идентична - через load.

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

Также и в CL. В одном файле 3 частично совпадающих кода: стадии компиляции, стадии загрузки и стадии выполнения.

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

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

Ну например - в общелиспе нельзя загрузить код at-compile, но не загружать at-runtime. Всмысле при загрузке at-compile код УЖЕ загружен и УЖЕ в рантайме.

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

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

Ну например - в общелиспе нельзя загрузить код at-compile, но не загружать at-runtime.

Можно.

(eval-when (:compile-toplevel)
   (defun macro-helper (...) ...))

Только если компиляции не будет, то macro-helper не загрузится. В отличие от Racket в CL компиляции может вообще не быть.

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

И нам нельзя получить «копию» модуля в рантайме

Это немножко не понял. Как эта копия получается в терминах Racket?

надо предусмотреть какие-то способы возвращения стейта к исходному состоянию

Стандартно:

; если один модуль
(asdf:oos 'asdf:clean-op :my-module)
(asdf:oos 'asdf:load-op :my-module)

; если всё
(asdf:oos 'asdf:load-op :my-module :force t)

Кстати, в Racket есть аналог clean-op? Или только вручную удалять *.zo?

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

Всмысле при загрузке at-compile код УЖЕ загружен и УЖЕ в рантайме.

Нет. Если у тебя в файле (defun foo ...) и ты вызовешь compile-file, то foo у тебя в текущем образе не загрузится.

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

Нет. Если у тебя в файле (defun foo ...) и ты вызовешь compile-file, то foo у тебя в текущем образе не загрузится.

Нет, речь о том что у меня в файле (eval-when (:compile-toplevel) (defun foo ...)). Оно будет вызываться исключительно при компиляции, но не будет вызываться при load - и у меня _нет_ способа корректно выполнить форму и при компиляции и в рантайме одновременно (потому что стейт общий).

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

_нет_ способа корректно выполнить форму и при компиляции и в рантайме одновременно

(eval-when (:compile-toplevel :load-toplevel :execute) (defun foo ...))

Или я тебя опять не понял?

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

Это немножко не понял. Как эта копия получается в терминах Racket?

> (module m racket
    (provide add d)
    (define n 0)
    (define (add) 
      (set! n (add1 n)))
    (define (d) 
      (displayln n)))
> (require 'm)
> (require (for-syntax 'm))
> (add)
> (add)
> (add)
> (d)
3
> (begin-for-syntax (add))
> (begin-for-syntax (add))
> (begin-for-syntax (d))
2
> (d)
3
> 

Стандартно:

Так образ остается засран, при чем тут файлы? Надо именно писать для модуля специально код, который откатит этот образ к начальному состоянию, руками следить за тем, чтобы оно все было корректно вызвано при том же:

(asdf:oos 'asdf:clean-op :my-module)
(asdf:oos 'asdf:load-op :my-module)
морока, короче

Кстати, в Racket есть аналог clean-op? Или только вручную удалять *.zo?

А зачем может понадобиться вручную удалять .zo?

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

Или я тебя опять не понял?

Ключевое слово - _корректно_. При выполнении в компайлтайме стейт засран, в результате в рантайме форму либо запускать нлеьзя, либо она будет работать некорректно.

А нам надо запустить корректно.

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

это враппер (module 'anonymous racket code ...)

Я это и имею в виду. Один и тот же код в REPL или через load работает, а если его же завернуть в модуль, то уже не работает. Причём, для сторонних языков есть ещё заморочка, что

#lang mylang
(begin
  foo bar baz)

и

#lang mylang
foo bar baz

Дают разный код, так как в первом случае будет #%plain-module-begin, а во втором #%module-begin и, если автор языка не озаботился обработкой особого случая, то облом.

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