Макросы. Наброски к докладу.
Хм, пришла идея: если я считаю авторитетными некоторые документы в сети, например, фейнмановскую автобиографическую книжку, вероятно мне будет интересно прочитать также, например, статьи, которые на нее ссылаются, например http://www.b-list.org/weblog/2015/oct/19/destroy-all-hiring-processes/ , поскольку можно ожидать там похожий ход мысли. Надо бы запилить краулер, чтобы проверить эту идею..
Наиболее общее определение: Правило или шаблон, определяющий, как некую подпоследовательность, поступающую на “вход” следует превратить в подпоследовательность на “выходе”.
Note(Mary): Странное определение, почему именно ПОДпоследовательность? И подпоследовательность чего?
Таким образом макрос - это отображение. Как правило, выходная подпоследовательность больше входной, поэтому процесс отображения часто называют “раскрытием”, macroexpansion.
Простейший способ сделать макрос - это перед компиляцией пропустить сорцы через препроцессор на регексах, но - это плохой негодный способ. Однако рабочий :)
Почему же он плох? Потому что:
- Препроцессор работает вне контекста исходника, что ограничивает его возможности. Можно конечно накрутить лексер и парсер и дать ему этот контекст, но… см. 10-е правило Гринспена.
- Препроцессор работает со входом и выходом на языке символьных подстановок, в то время как эффективнее было бы делать структурные подстановки - т.е. заниматься трансформациями AST
- Препроцессор - это как правило специальный язык (DSL) с отличающейся семантикой и синтаксисом. Это вызывает некоторые когнитивные сложности у слабых духом или волей программистов :) Кроме того часто отдельные утилитарные куски приходится реализовывать как на основном языке так и на языке препроцессора - а это как-то некругло - мы же хотим сократить себе работу а не увеличивать ее.
Закат солнца вручную.
#define MAX(x,y) ((x)>(y))?(x):(y)
t=MAX(i,s[i]);
=>
t=((i)>(s[i])?(i):(s[i]);
Макроопределения и макровызовы (с параметрами):
define(name [, expansion]) define('exch', '$2, $1') => exch('arg1', 'arg2') => arg2, arg1
Макросы аналогичны директивам препроцессора #define в C и в то же время аналогичны eval. Отличие от #define состоит в том, что для #define доступны 3-4 операции (арифметические действия над константами, вызов других макросов и две конкатенации).
Что может m4?
- Назначение/удаление переменных/макросов (define/undefile)
- И в стеке тоже (pushdef/popdef)
- Условное раскрытие (ifdef)
- Цикл и рекурсия (shift, forloop)
- Управление 10 виртуальными потоками вывода
- Вычисление выражений (eval)
- Операции со строками (substr, len, regexp, patsubst)
- Подстановка содержимого файлов (include)
- Выполнение shell-команд (syscmd)
- Коссвенные вызовы макросов (indir), в т.ч. встроенных (buildin)
- Трассировка раскрытия (traceon/traceoff)
Что им можно сделать?
- Генерация кода из декларативной спецификации
- Генерация SQL во время компиляции
- Документирование
- Администрирование (конфиги)
Note(Mary): Я считаю, что не стоит обо всём этом рассказывать, слишком долго. Достаточно примера и первого абзаца.
Макрос в лиспе — это своего рода функция, которая получает в качестве аргументов лисповские формы или объекты и, как правило, генерирует код, который затем будет скомпилирован и выполнен. Это происходит до выполнения программы, во время фазы, которая называется развёрткой макросов (macroexpansion). Макросы могут выполнять какие-то вычисления во время развёртки, используя полные возможности языка.
Макроопределения и макроподстановки:
(defparameter *dbg-enable* t)
(defparameter *dbg-indent* 1)
(defun dbgout (out)
(when *dbg-enable*
(format t (format nil "~~%~~~AT~~A" *dbg-indent*) out)))
(defmacro dbg (frmt &rest params)
`(dbgout (format nil ,frmt ,@params)))
(macroexpand-1 '(dbg "~A~A~{~A~^,~}" "this is debug message " "15:57:02 " (list 1 2 3 4)))
=> (DBGOUT
(FORMAT NIL "~A~A~{~A~^,~}" "this is debug message " "15:57:02 "
(LIST 1 2 3 4))), T
(dbg "~A~A~{~A~^,~}" "this is debug message " "15:57:02 " (list 1 2 3 4))
=> this is debug message 15:57:02 1,2,3,4
Q(Mary): Что в этом макросе такого, что нельзя сделать просто функцией? R: Если в compile-time dbg-enable = 0 - то отладочного вывода вообще нет в результирующем коде. Это эквивалент отладочных макросов в си и макроассемблерах. Note(Mary): Да, точно. Об этом обязательно стоит сказать, превентивно.
NOTE:рассказать про лексичекие переменные и другие области видимости
Результат раскрытия макроса выполняется в лексической среде места раскрытия — это важно, чтобы макрос мог изменить значение лексической переменной (для определения некоторых таких макросов в стандарте даже определён вспомогательный макрос define-modify-macro) — и сам может дополнять эту среду для переданного ему фрагмента кода (красивый пример: как в CL вызывать функции из переменных наподобие Scheme, без funcall http://www.xach.com/naggum/articles/3225069211869395@naggum.net.html)
[TODO] - из обсуждения http://lisper.ru/forum/thread/1079
Если вы облицовываете пол плиткой, размер которой с ноготь, вы не тратите излишних усилий – Пол Грэм
В то время, как в других языках у вас есть небольшие квадратные плиточки, в Lisp вы можете выбрать плитку любого размера и любой формы.
Здесь надо добавить какой-нибудь элегантный макрос, который выполняет преобразования над AST: профайлинг, логгинг, аспекты.
Вот тут будет сложно… Это кусок моего проекта по автоматизированному поиску работы. Он трансформирует, упрощаяя DOM-дерево страницы с вакансиями так, чтобы по нему можно было собрать информацию о собственно размещенных вакансиях и компаниях, их разместивших.
Первая функция получает на вход DOM-дерево и функцию трансформер-предикат. Если полученное дерево матчится с трансформером-предикатом, то возвращается преобразованное дерево, в противном случае возвращаем входное дерево “как есть”.
Таким образом можно последовательно прогонять одно и то же DOM-дерево не просто через цепочку упрощающих преобразований, а через граф с if-ветвлениями.
Второй макрос - это просто синтаксический сахар, чтобы вызывать первую функцию покомпактнее. Содержит гигиену :) Ну а дальше идет собственно преобразование - и оно настолько объемное, что я прямо не знаю, как я бы справился без этого самонаписанного синтаксического сахара… Ну и результат полного макрораскрытия смотрится настолько эпично, что у меня даже редактор зависает при попытке захайлайтить такую гору кода
(in-package #:moto)
(ql:quickload "split-sequence")
;; Это аналог maptree-if, но здесь одна функция и ищет и трансформирует узел дерева
(defun maptree (predicate-transformer tree)
(multiple-value-bind (t-tree control)
(aif (funcall predicate-transformer tree)
it
(values tree #'mapcar))
(if (and (consp t-tree)
control)
(funcall control
#'(lambda (x)
(maptree predicate-transformer x))
t-tree)
t-tree)))
;; maptree-transformer - синтаксический сахар для maptree
(defmacro mtm (transformer tree)
(let ((lambda-param (gensym)))
`(maptree #'(lambda (,lambda-param)
(values (match ,lambda-param ,transformer)
#'mapcar))
,tree)))
(print
(SB-CLTL2:MACROEXPAND-ALL
'(mtm (`("div" (("class" "search-result") ("data-qa" "vacancy-serp__results")) ,@rest) rest)
(mtm (`("div" (("data-qa" ,_) ("class" ,(or "search-result-item search-result-item_premium search-result-item_premium"
"search-result-item search-result-item_standard "
"search-result-item search-result-item_standard_plus "))) ,@rest)
(let ((in (remove-if #'(lambda (x) (or (equal x 'z) (equal x "noindex") (equal x "/noindex"))) rest)))
(if (not (equal 1 (length in)))
(progn (print in)
(err "parsing failed, data printed"))
(car in))))
(mtm (`("a" (("class" _) ("href" _) ("data-qa" "vacancy-serp__vacancy-interview-insider"))
"Посмотреть интервью о жизни в компании") 'Z)
(mtm (`("a" (("href" ,_) ("target" "_blank") ("class" "search-result-item__label search-result-item__label_invited")
("data-qa" "vacancy-serp__vacancy_invited")) "Вы приглашены!") 'Z)
(mtm (`("a" (("href" ,_) ("target" "_blank") ("class" "search-result-item__label search-result-item__label_discard")
("data-qa" "vacancy-serp__vacancy_rejected")) "Вам отказали") 'Z)
(mtm (`("a" (("href" ,_) ("target" "_blank") ("class" "search-result-item__label search-result-item__label_discard")
("data-qa" "vacancy-serp__vacancy_rejected")) "Вам отказали") 'Z)
(mtm (`("a" (("title" "Премия HRBrand") ("href" ,_) ("rel" "nofollow")
("class" ,_)
("data-qa" ,_)) " ") 'Z)
(mtm (`("div" (("class" "search-result-item__image")) ,_) 'Z)
(mtm (`("script" (("data-name" "HH/VacancyResponseTrigger") ("data-params" ""))) 'Z)
(mtm (`("a" (("href" ,_) ("target" "_blank") ("class" ,_)
("data-qa" "vacancy-serp__vacancy_responded")) "Вы откликнулись") 'Z)
(mtm (`("div" (("class" "search-result-item__star")) ,@_) 'Z)
(mtm (`("div" (("class" "search-result-item__description")) ,@rest)
(loop :for item :in rest :when (consp item) :append item))
(mtm (`("div" (("class" "search-result-item__head"))
("a" (("class" ,(or "search-result-item__name search-result-item__name_standard"
"search-result-item__name search-result-item__name_standard_plus"
"search-result-item__name search-result-item__name_premium"))
("data-qa" "vacancy-serp__vacancy-title") ("href" ,id) ("target" "_blank")) ,name))
(list :id (parse-integer (car (last (split-sequence:split-sequence #\/ id)))) :name name))
(mtm (`("a" (("class" "interview-insider__link m-interview-insider__link-searchresult")
("href" ,href)
("data-qa" "vacancy-serp__vacancy-interview-insider"))
"Посмотреть интервью о жизни в компании")
(list :interview href))
(mtm (`("div" (("class" "b-vacancy-list-salary") ("data-qa" "vacancy-serp__vacancy-compensation"))
("meta" (("itemprop" "salaryCurrency") ("content" ,currency)))
("meta" (("itemprop" "baseSalary") ("content" ,salary))) ,salary-text)
(list :currency currency :salary (parse-integer salary) :salary-text salary-text))
(mtm (`("div" (("class" "search-result-item__company")) ,emp-name)
(list :emp-name emp-name))
(mtm (`("div" (("class" "search-result-item__company"))
("a" (("href" ,emp-id)
("class" "search-result-item__company-link")
("data-qa" "vacancy-serp__vacancy-employer"))
,emp-name))
(list :emp-id (parse-integer (car (last (split-sequence:split-sequence #\/ emp-id)))
:junk-allowed t)
:emp-name emp-name))
(mtm (`("div" (("class" "search-result-item__info")) ,@rest)
(loop :for item :in rest :when (consp item) :append item))
(mtm (`("span" (("class" "searchresult__address")
("data-qa" "vacancy-serp__vacancy-address")) ,city ,@rest)
(let ((metro (loop :for item in rest :do
(when (and (consp item) (equal :metro (car item)))
(return (cadr item))))))
(list :city city :metro metro)))
(mtm (`("span" (("class" "metro-station"))
("span" (("class" "metro-point") ("style" ,_))) ,metro)
(list :metro metro))
(mtm (`("span" (("class" "b-vacancy-list-date")
("data-qa" "vacancy-serp__vacancy-date")) ,date)
(list :date date))
(mtm (`("span"
(("class" "vacancy-list-platform")
("data-qa" "vacancy-serp__vacancy_career"))
" • " ("span" (("class" "vacancy-list-platform__name"))
"CAREER.RU"))
(list :platform 'career.ru))
(block subtree-extract
(mtm (`("div"
(("class" "search-result")
("data-qa" "vacancy-serp__results"))
,@rest)
(return-from subtree-extract rest))
""))))))))))))))))))))))))))
=> 2200 строк раскрытия...
Note(Mary): Это жестоко. Это не читаемо. Это категорически нельзя показывать, только если ты не скажешь, что 20 строк могут развернуться в 2200, но без подробностей. R: Хм, а как показать? Или может рассказать, как я рассказал тебе в слаке - про то как это работает? А код оставить для иллюстрации? Note(Mary): Мне кажется, достаточно показать тот макрос (не объясняя, просто дать оценить размер), а потом сказать, что он раскрывается на 2200 с хвостом строк.
(defmacro !1 (x)
(if (= x 1)
1
`(* ,x (!1 ,(1- x)))))
(macroexpand-all '(!1 5))
(SB-CLTL2:MACROEXPAND-ALL '(!1 5))
=> (* 5 (* 4 (* 3 (* 2 1))))
Note(Mary): А вот это милый макрос, его вполне можно показать.
тодо - объяснить про квазицитирование
(defmacro defsynonym (old-name new-name)
"Define OLD-NAME to be equivalent to NEW-NAME when used in the first position of a Lisp form."
`(defmacro, new-name (&rest args)
`(,',old-name ,@args)))
=> DEFSYNONYM
(macroexpand-1 '
(defsynonym cons make-pair))
=>(DEFMACRO MAKE-PAIR (&REST ARGS) `(CONS ,@ARGS)), T
(defsynonym cons make-pair)
=>MAKE-PAIR
(make-pair 'a 'b)
=> (A . B)
Note(Mary): Тоже воспринимаемо и может быть показано.
Когда eval
получает список, у которого car
элемент является
символом, она ищет локальные определения для этого символа (flet,
labels и macrolet). Если поиски не увенчались успехом, она ищет
глобальное определение. Если это глобальное определение является
макросом, тогда исходный список называется макровызовом.
С определением будет ассоциирована функция двух аргументов, называемая функцией раскрытия. Эта функция вызывается с макровызовом в качестве первого аргумента и лексическим окружением в качестве второго. Функция должна вернуть новую Lisp’овую форму, называемую раскрытием макровызова. (На самом деле участвует более общий механизм, см. macroexpand) Затем это раскрытие выполняется по месту оригинальной (исходной) формы.
Когда функция компилируется, все макросы, в ней содержащиеся, раскрываются во время компиляции. Это значит, что определение макроса должно быть прочитано компилятором до его первого использования.
Реализация Common Lisp’а имеет большую свободу в выборе того, когда в программе раскрываются макровызовы. Например, допускается для оператора defun раскрытие всех внутренних макровызовов в время выполнения формы defun и записи полностью раскрытого тела функции, как определение данной функции для дальнейшего использования. (Реализация может даже выбрать путь, все время компилировать функции определённые с помощью defun, даже в режиме «интерпретации».)
Для правильного раскрытия макросы должны быть написаны так, чтобы иметь наименьшие зависимости от выполняемого окружения. Лучше всего удостовериться, что все определения макросов доступны перед тем, как компилятор или интерпретатор будет обрабатывает код, содержащий макровызовы к ним.
В Common Lisp, макросы не являются функциями. В частности, макросы не могут использоваться, как функциональные аргументы к таким функциям, как apply, funcall или map. В таких ситуациях список, отображающий “первоначальный макровызов” не существует и не может существовать, потому что в некотором смысле аргументы уже были вычислены.
Note(Mary): Злостная копипаста :) Надо как-то из этого извлечь главное. R:Вообще, там есть зайчатки интересных технологий. Например мы можем злобно залезть и заменить функцию раскрытия прямо таки похачив уже определенные макросы. И мы можем даже хачить таким образом тот макрос, который в данный момент раскрывается )))
Note(Mary): Кстати говоря, вот это очень важный вопрос. Код с макросами отлаживать очень тяжело, но надо.
Тут нужна картинка вида “Гарри Поттер и философский камень”
- read
- macro expansion
- compilation
- loading
- execute
Стадии могут чередоваться: каждая форма верхнего уровня (top-level form) проходит стадии обработки кода, и только затем читается следующая форма. Это дает возможность производить какие-либо побочные эффекты, которые могут повлиять на обработку следующей формы. Например, если файл компилируется с помощью compile-file, то каждая форма проходит следующие стадии: чтение, раскрытие макросов, компиляция, и только при вызове load для скомпилированного fasl’а будут произведены эффекты времени загрузки; если файл загружается с помощью load, то каждая форма проходит через стадии: чтение, раскрытие макросов, компиляция, загрузка; если формы набираются в REPL, то форма проходит все стадии от чтения до исполнения. Поэтому, в зависимости от способа ввода кода (ввод в REPL; загрузка с помощью LOAD; компиляция и загрузка с помощью (LOAD (COMPILE-FILE ..)); вызов EVAL или COMPILE для формы), эффекты от него могут быть различными, так как побочные эффекты от разных форм будут наступать в разное время (чаще всего, разница будет в том, что будут ошибки компиляции либо загрузки)
Например: defpackage, in-package производят побочные эффекты на стадиях компиляции и загрузки, поэтому во время компиляции файла компилятор уже имеет созданный пакет, и символы будут читаться в указанный пакет. Форма defun производит свой основной побочный эффект (определение функции) во время компиляции - поэтому при компиляции файла макросы не видят функции, определенные в этом же файле.
Чтение - читается символьный поток и возвращается в виде cons-ячеек, содержащих s-выражения. Во время чтения может выполняться код, определяемый выражениями #. и текущей таблицей чтения (READTABLE). Это дает возможность (хотя и довольно неудобную) компилировать код, записанный каким-либо другим синтаксисом (см., например, http://kpreid.livejournal.com/14713.html)
Вторая стадия обработки кода (сразу после чтения формы) - раскрытие макросов. То, как проходит раскрытие макросов, определяется макросами, определенными через DEFMACRO, DEFINE-SYMBOL-MACRO и их лексическими вариантами MACROLET, SYMBOL-MACROLET, а также макросами, определенными с помощью DEFINE-SETF-EXPANDER и DEFINE-MODIFY-MACRO, макросами компиляции DEFINE-COMPILER-MACRO и динамической переменной MACROEXPAND-HOOK. Макросы лиспа являются одновренно и всемогущими (в принципе, способны осуществить любой преобразование кода), но также ничего не знающими (так как не могут анализировать окружающий лексический контекст, не прибегая к реализации полного code-walker’а для CL или к расширениям стандарта (примечание: в CLtL2 определены функции для анализа лексического контекста, но в CL они не включены; в ряде реализаций они присутствуют, например, в пакете SB-CLTL2)). Вследствие этого появляются неудобства, связанные с отсутствием гигиены, сложностью отслеживания ошибок, но, что самое важное, становится невозможно описывать нелокальные преобразования кода модульным образом, не прибегая к переписыванию системы обработки кода или к управлению ей (но это тоже проблематично: так как MACROEXPAND-HOOK не вызывается для специальных и обычных форм, то необходимо модифицировать читатель, чтобы можно было обрабатывать все формы, не заставляя пользователя оборачивать каждую форму в какой-нибудь “волшебный” макрос-обертку).
Затем идут следующие стадии обработки: либо компиляция, после которой следует или не следует загрузка, или же непосредственное исполнение без компиляции. Происходящие стадии могут быть перемешанными между собой: по стандарту допускается начать компиляцию или исполнение формы, когда в ней еще не до конца раскрыты все макросы, либо же можно сперва раскрыть все макросы и только потом компилировать (конечно, раскрытие макросов требует анализа лексической области действия, чтобы отличать макросы от обычных выражений).
Если код вводится в REPLе или с помощью LOAD загружается исходный текст или с помощью EVAL либо вычисляется форма, то код проходит только стадию исполнения (и не проходит стадии компиляции или загрузки). Если встречается EVAL-WHEN с параметром :EXECUTE, то он превращается просто в PROGN, и иначе в NIL. Это же может происходить вперемешку с раскрытием макросов; например, SBCL может начать вычислять выражение (when nil (foo)) и вернуть nil, не раскрывая макрос (foo); поэтому, если ожидалось выполнения побочных эффектов от этого макроса, их не будет (мы тоже этому удивились, когда тестировали ASDF-DEPENDENCY-GROVEL).
Если вы компилирует код с помощью COMPILE, то этот код будет исполнен во время стадии исполнения (:EXECUTE), поэтому если он содержит EVAL-WHEN, то он ведет себя аналогично предыдущему случаю. Так как компилируемый код всегда является функцией (именованной или безымянной), то в этом коде нет формы верхнего уровня (toplevel form), поэтому указание стадий :COMPILE-TOPLEVEL и :LOAD-TOPLEVEL не имеет смысла и игнорируется. Если я правильно понимаю, то компилятор может не раскрывать макросы, если он может статически доказать, что они находятся в недостижимом коде; однако на практике компиляторы работают в несколько проходов, и макросы раскрываются полностью, прежде чем код анализируется на наличие недостижимых частей кода.
Иная ситуация наблюдается, когда EVAL-WHEN встречается в коде, который сперва компилируется с помощью COMPILE-FILE, и затем полученный FASL загружается с помощью LOAD. В этом случае, каждая форма после раскрытия макросов обрабатывается таким образом, что отделяются побочные эффекты, которые происходят во время компиляции от эффектов, происходящих во время загрузки. Если указать :COMPILE-TOPLEVEL в EVAL-WHEN, то побочные эффекта кода, заключенного в EVAL-WHEN, будут происходить во время компиляции (т.е., в текущем образе, а также сохранятся в CFASL (которые поддерживаются с SBCL-1.0.30.4) и будет воспроизведены при загрузке указанного CFASL). Если указать :LOAD-TOPLEVEL, то побочные эффекты кода будут происходить во время загрузки (т.е., они сохраняются в FASL и произойдут при загрузке FASL, но они не будут происходить в текущем образе, если также не указана стадия :COMPILE-TOPLEVEL). Некоторые специальные формы имеют побочные эффекты как во время компиляции, так и во время загрузки, например IN-PACKAGE, которая меняет текущий пакет (PACKAGE) во время компиляции и во время загрузки; DEFVAR объявляет переменную специальной как во время компиляции (в текущем образе), так и во время загрузки (в том образе, в который будет загружаться FASL), а также устанавливает значение во время загрузки. Указание :EXECUTE для форм верхнего уровня игнорируется (но во вложенном EVAL-WHEN имеет смысл использовать только :EXECUTE).
На практике, стоит запомнить, что единственная безопасная и полезная комбинация параметров - это (EVAL-WHEN (:COMPILE-TOPLEVEL :LOAD-TOPLEVEL :EXECUTE) …), в который следует заворачивать вещи, которые должны быть доступны во время компиляции и во время работы кода такие: например, объявления функций, переменных и побочных эффектов, которые используются макросами.
Использовать (:LOAD-TOPLEVEL :EXECUTE) безопасно, но любая форма верхнего уровня уже неявно обернута в (EVAL-WHEN (:LOAD-TOPLEVEL :EXECUTE) ..), поэтому использовать эту комбинацию не имеет смысла (за исключением ситуации, когда форма расположена внутри EVAL-WHEN с другими параметрами).
Другая безопасная комбинация параметров - (:COMPILE-TOPLEVEL :EXECUTE), но польза от нее ограничена. Ее можно использовать для того, чтобы побочные эффекты от выполнения кода были только в среде компиляции; например, изменение таблицы чтения (readtable). Но если такой побочный эффект произойдет во время компиляции файла и сохранится в сеансе работы (например, если изменять значение какой-либо переменной, для которой создаются локальные привязки во время компиляции, например READTABLE, то изменения не сохранятся после компиляции), то во время загрузки скомпилированного FASLа этого изменения может не быть (если FASL загружен из другого сеанса), что может создать непонятные проблемы при компиляции и сборке программ. Недетерминированные действия во время компиляции (например, использование файловой системы) - это плохой вкус. Если требуется вычислить что-либо детерминированно, то это можно сделать и во время чтения, а если недетерминированно, то стоит отложить вычисления на более позднее время (например, провести вычисления во время сохранения образа). Один из разумных вариантов использования (:COMPILE-TOPLEVEL :EXECUTE) - это сохранение побочных эффектов времени компиляции, когда для сборки используется XCVB с поддержкой механизма CFASL (который поддерживается в SBCL >= 1.0.30.4); при этом гарантируется, что при компиляции всех файлов, которые зависят от данного файла, эти побочные эффекты будут воспроизведены. В итоге, хотя использование (:COMPILE-TOPLEVEL :EXECUTE) безопасно, оно годится лишь для очень ограниченного числа случаев. Если вы не эксперт, то даже не пытайтесь.
Другие комбинации параметров EVAL-WHEN можно не рассматривать. Они бессмыслены, и имеют смысл разве что лишь гипотетически внутри низкоуровневого макроса оптимизации; всегда будет возможность загрузить код каким-либо образом, что побочные эффекты наступят неожиданно и приведут к неожиданным последствиям. У пользователя должна быть возможность, в зависимости от его нужд, компилировать и загружать код так, как он захочет - просто LOAD’ом, или же (LOAD (COMPILE-FILE …)), или же загрузка FASLа в новый образ или же инкрементальная рекомпиляция с помощью ASDF - код всегда должен загружаться и работать предсказуемо.
Когда загружается FASL или CFASL, происходят все сохраненные в нем эффекты: в пакеты добавляются символы, вычисляются выражения для LOAD-TIME-VALUE, добавляются определения переменных, макросов и функций, любые другие побочные эффекты от toplevel-форм. При этом, побочные эффекты стадии чтения и стадии раскрытия макросов не считаются эффектами времени компиляции или загрузки, и поэтому не проявляются при загрузке FASL или CFASL. На самом деле, это даже полезно, так как это позволяет делать что-либо во время чтения кода или при раскрытии макросов, и эти вычисления не будут заново производиться при загрузке кода. Например, SBCL (и другие вменяемые реализации) не будут повторять эффекты времени раскрытия макросов при загрузке кода (хотя, гипотетически, можно представить такую реализацию). Но если ваши макросы совершают какие-то побочные эффекты, которые не должны пропасть после компиляции, то макросы должны не только производить эти эффекты, но и раскрываться в код, который производит те же побочные эффекты во время компиляции и/или загрузки (используя EVAL-WHEN). В качестве примера: когда я переводил крупный проект с ASDF на XCVB, пришлось отлаживать макрос, который вызывал (EVAL (DEFCLASS …)) и FINALIZE-INHERITANCE во время раскрытия макроса, чтобы иметь возможность использовать MOP для анализа сгенерированного класса, но не включал DEFCLASS в раскрываемый код; в результате, при компиляции “с нуля”, макрос работал, но не работал при загрузке из FASLов (используя инкрементальную компиляцию в ASDF) или при детерминированной сборке (используя XCVB), так как другие макросы в других файлах ожидали, что класс будет определен (чего не происходило при загрузке из FASLов).
EVAL-WHEN легко использовать неправильно, и на самом деле у которого есть только одно разумное применение (если использовать XCVB, то два). Важно понимать, в каких случаях EVAL-WHEN нужен - прежде всего для объявления функций и переменных, которые используются макросами.
Тело формы eval-when выполняется как неявный progn, но только в перечисленных ниже ситуациях. Каждая ситуация situation должна быть одним символов, :compile-toplevel, :load-toplevel или :execute.
Использование :compile-toplevel и :load-toplevel контролирует, что и когда выполняется для форм верхнего уровня. Использование :execute контролирует будет ли производится выполнения форм не верхнего уровня.
Конструкция eval-when может быть более понятна в терминах модели того, как компилятор файлов, compile-file, выполняет формы в файле для компиляции.
Формы следующие друг за другом читаются из файла с помощью компилятора файла используя read. Эти формы верхнего уровня обычно обрабатываются в том, что мы называем режим «времени некомпиляции (not-compile-time mode)». Существует и другой режим, называемый режим «времени-компиляции (compile-time-too mode)», которые вступает в игру для форм верхнего уровня. Оператор eval-when используется выбора режима(ов), в котором происходит выполнение кода.
Обработка форм верхнего уровня в компиляторе файла работает так, как рассказано ниже:
- Если форма является макровызовом, она разворачивается и результат обрабатывается, как форма верхнего уровня в том же режиме обработки (времени-компиляции или времени-некомпиляции, (compile-time-too или not-compile-time).
- Если форма progn (или locally), каждая из форм из их тел обрабатываются, как формы верхнего уровня в том же режиме обработки.
- Если форма compiler-let, macrolet или symbol-macrolet, компилятор файла создаёт соответствующие связывания и рекурсивно обрабатывает тела форм, как неявный progn верхнего уровня в контексте установленных связей в том же режиме обработки.
- Если форма eval-when, она обрабатывается в соответствии со
следующей таблицей:
LT CT EX CTTM Действие да да – – обработать тело в режиме время-компиляции да нет да да обработать тело в режиме время-компиляции да нет – нет обработать тело в режиме время-некомпиляции да нет нет – обработать тело в режиме время-некомпиляции нет да – – выполнить тело нет нет да да выполнить тело нет нет – нет ничего не делать нет нет нет – ничего не делать В этой таблице столбец LT спрашивает присутствует ли :load-toplevel в ситуациях указанных в форме eval-when. CT соответственно указывает на :compile-toplevel и EX на :execute. Столбец CTTM спрашивает встречается ли форма eval-when в режиме времени-компиляции. Фраза «обработка тела» означает обработку последовательно форм тела, как неявного progn верхнего уровня в указанном режиме, и «выполнение тела» означает выполнение форм тела последовательно, как неявный progn в динамическом контексте выполнения компилятора и в лексическом окружении, в котором встретилась eval-when.
- В противном случае, форма верхнего уровня, которая не представлена в специальных случаях. Если в режиме времени-компиляции, компилятор сначала выполняет форму и затем выполняет обычную обработку компилятором. Если установлен режим времени-некомпиляции, выполняется только обычная обработка компилятором (смотрите раздел 24.1). Любые подформы обрабатываются как формы не верхнего уровня.
Следует отметить, что формы верхнего уровня обрабатываются гарантированно в порядке, в котором они были перечислены в тексте в файле, и каждая форма верхнего уровня прочтённая компилятором обрабатывается перед тем, как будет прочтена следующая. Однако, порядок обработки (включая, в частности, раскрытие макросов) подформ, которые не являются формами верхнего уровня, не определён.
Для формы eval-when, которая не является формой верхнего уровня в компиляторе файлов (то есть либо в интерпретаторе, либо compile, либо в компиляторе файлов, но не на верхнем уровне), если указана ситуация :execute, тело формы обрабатывается как неявный progn. В противном случае, тело игнорируется и форма eval-when имеет значение nil.
Для сохранения обратной совместимости, situation может также быть compile, load или eval. Внутри формы верхнего уровня eval-when, они имеют значения :compile-toplevel, :load-toplevel и :execute соответственно. Однако их поведение не определено при использовании в eval-when не верхнего уровня.
Следующие правила являются логическим продолжением предыдущих определений:
- Никогда не случится так, чтобы выполнение одного eval-when выражения приведёт к выполнению тела более чем один раз.
- Старый ключевой символ eval был неправильно использован, потому
что выполнение тела не нуждается в eval. Например, когда
определение функции
(defun foo () (eval-when (:execute) (print ’foo)))
скомпилируется, вызов print должен быть скомпилирован, а не выполнен во время компиляции. Макросы, предназначенные для использования в качестве форм верхнего уровня, должны контролировать все побочные эффекты, которые будут сделаны формами в процессе развёртывания. Разворачиватель макроса сам по себе не должен порождать никаких побочных эффектов.
(defmacro foo () (really-foo) ; Неправильно ‘(really-foo)) (defmacro foo () ‘(eval-when (:compile-toplevel :load-toplevel :execute) ; Правильно (really-foo)))
Соблюдение этого правила будет значит, что такие макросы будут вести себя интуитивно понятно при вызовах в формах не верхнего уровня.
- Расположение связывания переменной окружённой eval-when
захватывает связывание, потому что режим «время-компиляции» не
может случиться (потому что eval-when не может быть формой
верхнего уровня)
(let ((x 3)) (eval-when (:compile-toplevel :load-toplevel :execute) (print x)))
выведет 3 во время выполнения (в данном случае загрузки) и не будет ничего выводить во время компиляции. Разворачивание defun и defmacro может быть выполнено в контексте eval-when и могут корректно захватывать лексическое окружение. Например, реализация может разворачивать форму defun, такую как:
(defun bar (x) (defun foo () (+ x 3)))
(progn (eval-when (:compile-toplevel)
(compiler::notice-function ’bar ’(x)))
(eval-when (:load-toplevel :execute)
(setf (symbol-function ’bar)
#’(lambda (x)
(progn (eval-when (:compile-toplevel)
(compiler::notice-function ’foo
’()))
(eval-when (:load-toplevel :execute)
(setf (symbol-function ’foo)
#’(lambda () (+ x 3)))))))))
которая по предыдущим правилам будет обработана также, как и
(progn (eval-when (:compile-toplevel)
(compiler::notice-function ’bar ’(x)))
(eval-when (:load-toplevel :execute)
(setf (symbol-function ’bar)
#’(lambda (x)
(progn (eval-when (:load-toplevel :execute)
(setf (symbol-function ’foo)
#’(lambda () (+ x 3)))))))))
Вот несколько дополнительных примеров.
(let ((x 1))
(eval-when (:execute :load-toplevel :compile-toplevel)
(setf (symbol-function ’foo1) #’(lambda () x))))
eval-when в предыдущем выражении не является формой верхнего уровня, таким образом во внимание берётся только ключевой символ :execute. это не будет иметь эффекта во время компиляции. Однако этот код установит в (symbol-function ’foo1) функцию которая возвращает 1 во время загрузки (если let форма верхнего уровня) или во время выполнения (если форма let вложена в какую-либо другую форму, которая ещё не была выполнена).
(eval-when (:execute :load-toplevel :compile-toplevel)
(let ((x 2))
(eval-when (:execute :load-toplevel :compile-toplevel)
(setf (symbol-function ’foo2) #’(lambda () x)))))
Если предыдущее выражение находилось на верхнем уровне в компилируемом файле, оно будет выполнятся в обоих случаях, и во время компиляции и во время загрузки.
(eval-when (:execute :load-toplevel :compile-toplevel)
(setf (symbol-function ’foo3) #’(lambda () 3)))
Если предыдущее выражение находилось на верхнем уровне в компилируемом файле, оно будет выполняться в обоих случаях, и во время компиляции и во время загрузки.
(eval-when (:compile-toplevel)
(eval-when (:compile-toplevel)
(print ’foo4)))
Предыдущее выражение ничего не делает, оно просто возвращает nil.
(eval-when (:compile-toplevel)
(eval-when (:execute)
(print ’foo5)))
Если предыдущее выражение находилось на верхнем уровне в компилируемом файле, foo5 будет выведено во время компиляции. Если эта форма была не на верхнем уровне, ничего не будет выведено во время компиляции. Вне зависимости от контекста, ничего не будет выведено во время загрузки или выполнения.
(eval-when (:execute :load-toplevel)
(eval-when (:compile-toplevel)
(print ’foo6)))
Если предыдущая форма находилась на верхнем уровне в компилируемом файле, foo6 будет выведено во время компиляции. Если форма была не на верхнем уровне, ничего не будет выведено во время компиляции. Вне зависимости от контекста, ничего не будет выведение во время загрузки или выполнения кода.
Note(Mary): Разумеется, не осилила. Это даже читать стоит только тогда, когда ты очень хочешь разобраться в деталях. Если ты просто пришёл послушать, что это за штука такая - тебе эти тонкости ни к чему, только отпугнут. R: Да, это тонкости, но их нужно знать мне, чтобы ответить на каверзные вопросы о том, как и когда происходит раскрытие, как этим управлять и что может произойти Note(Mary): А, хорошо.
Во время случайного рабочего скриптинга созрел интересный и наглядный пример применения этих кложурных макросов.
Задача была простая - посчитать в файле кол-во строк, совпадающих с шаблоном, но перед этим пришлось профильтровать от всяких непечатных символов.
И действительно, разворачивая каскад фильтров в последовательность можно их сколько угодно написать не беспокоясь о читабельности.
Вопрос только в том, правильно ли я поименовал этот макрос - в кложе макрос => подставляет свой следующий аргумент в конец формы предыдущего или как-то иначе? Если так, то как в кложе называется макрос, который работает как этот?
(ql:quickload "alexandria")
(ql:quickload "split-sequence")
(ql:quickload "cells")
;; тестовые данные
(defparameter *test*
"гарант
другие
другие
гарант
гарант
гарант
гарант
гарант
другие
гарант
другие
другие")
;; Пример раз: тот, который в презентации. Там есть несколько функций, которые принимают нечто типа A первым аргументом, плюс несколько других аргументов (>= 0), и возвращают снова нечто типа А.
;; (-> (a) (b c)) == (b a c)
;; (-> (a) (b c) (d e)) == (d (b a c) e)
(defun ->helper (forms)
(if (null (cdr forms))
(car forms)
(list* (caar forms)
(->helper (cdr forms))
(cdar forms))))
;; собственно макрос ->
(defmacro -> (&body forms)
(let ((forms (reverse forms)))
(->helper forms)))
;; Пример применения
(->
(split-sequence:split-sequence #\LineFeed *test*)
(remove-duplicates :test #'string=))
;; Пример два: несколько функций, которые принимают нечто типа А последним аргументом и возвращают снова нечто типа А. Это как раз то, что принято в Лиспе (и в Хаскеле тоже) со списками (в Хаскеле на этом удобно каррирование строить).
;; (-» (a) (b c)) == (b c a)
;; (-» (a) (b c) (d e)) == (d e (b c a))
;; хелпер для обеспечения рекурсивности макроса ->>
;; который выполняет всю работу
(defun ->>helper (forms)
(if (null (cdr forms))
(car forms)
(append (car forms)
(list (->>helper (cdr forms))))))
;; собственно макрос ->>
(defmacro ->> (&body forms)
(let ((forms (reverse forms)))
(->>helper forms)))
;; Тестовый пример краткой записи с макросом ->>
;; (print (sb-cltl2:macroexpand-all '
(let ((garant 0) (other 0))
(->> *test*
(split-sequence:split-sequence #\LineFeed)
(mapcar #'(lambda (str)
(->>
(concatenate 'list str)
(remove #\Return)
(remove #\ZERO_WIDTH_NO-BREAK_SPACE)
(concatenate 'string)
)))
(mapcar #'(lambda (str)
(if (string= "гарант" str)
(incf garant)
(incf other))))
)
(cons garant other))
;; ))
;; Его раскрытие в каскад фильтров
(let ((garant 0) (other 0))
(mapcar #'(lambda (str)
(if (string= "гарант" str)
(incf garant)
(incf other)))
(mapcar
#'(lambda (str)
(concatenate 'string
(remove #\zero_width_no-break_space
(remove #\return (concatenate 'list str)))))
(split-sequence:split-sequence #\newline *test*)))
(cons garant other))
Развиваем тему… Теперь мне бы хотелось сделать макрос, который реализует такие подстановки не в строго определенное место, а куда захочет пользователь. Например заменяя :~ на вставляемое выражение.
(~> (get-source-list param)
(first-filter some-param-1 :~ some-param-3)
(second-filter some-param-1 some-param-2 :~ some-param-4))
Следующий шаг, как я полагаю - вспомнить о том, что вызов может возвращать несколько значений. Мы могли бы эти значения как-то аккумулировать, чтобы потом организовывать на базе них граф вычислений
(~> :retval-1 :retval-2 (get-source-list param)
:retval-3 (first-filter some-param-1 :retval-1 some-param-3 :retval-3)
(second-filter some-param-1 some-param-2 :retval-1 :retval-3 some-param-4))
Тут макрос мог бы строить на базе этого AST. Если добавить к этому ленивость и вычисления по запросу - то мы уже получаем реализацию чего-то похожего на CELLS https://common-lisp.net/project/cells/
(по swizard-у - http://swizard.info/articles/solitaire/article.html)
Note(Mary): Не со всем согласна. Например, очень странная мысль: “Действительно, какая разница: будет проект компилироваться десять секунд или десять минут?” - он явно не занимался интенсивной разработкой.
Note(Rigidus): А проект не должен вообще компилироваться - он должен разрабатываться в репле. All compilation must be incremental!
Note(Mary): Ах да, репл, динамика… Я как-то больше доверяю компилируемым программам, к которым нельзя подключиться и всё сломать :) Но инкрементальная компиляция сама по себе - отличная штука :)
Q(Mary): А где именно там пример оптимизации кода на лету?
A(Rigidus): А тут пока [TODO]
Что я собственно имею ввиду?
Ну вот допустим у нас есть некий метод
(defmethod some-test (alfa beta gamma)
(* (+ alfa beta) gamma))
Если мы попытаемся его каррировать, то получим что-то вроде:
(defun curry (function &rest initial-args)
"Returns a function which will call FUNCTION passing it INITIAL-ARGS and then any other args.
(funcall (curry #'list 1) 2) ==> (list 1 2)"
(lambda (&rest args)
(apply function (append initial-args args))))
(funcall (curry #'some-test 3) 4 5)
=> 35
Но это просто обертка над some-test
, и даже не макрос. Попробуем
сделать макросом, так, чтобы:
(macroexpand-1 '(curry-macro #'some-test 3))
=>
(defmethod some-test (beta gamma)
(* (+ 3 beta) gamma))
Когда мы это сдели - остается применить правила оптимизации к выходному AST чтобы постараться все что возможно вычислить в macroexpand-time.
Note(Mary): Если ты хочешь на базе статьи рассказывать про DSL, то не рекомендую, там слишком долго объяснять придётся, либо никто ничего не поймёт.
R: Да, но рассказать то надо..
Note(Mary): Тут уместно вспомнить про -> и ->> из кложуры. Вполне себе DSL.
R:А где это посмотреть? Расскажи мне!
Note(Mary): https://clojuredocs.org/clojure.core/-%3E https://clojuredocs.org/clojure.core/-%3E%3E
Идея гигиены - отделить окружение макроса от окружения его продукции, и таким образом избежать возможных пересечений определяемых переменных.
В Scheme эта идея прижиалась, но она мешает анафорическим макросам
Самый простой пример анафорического макроса: АIF (или IF-IT), который тестирует первый аргумент на истинность и одновременно привязывает его значение к переменной IT, которую, соответственно, можно использовать в THEN-clause:
(defmacro aif (var then &optional else)
`(let ((it ,var))
(if it ,then ,else)))
Однако на самом деле и в Scheme не так уж сложно добиться аналогичных макросов (см. http://www.greghendershott.com/fear-of-macros/Syntax_parameters.html). Ключевое отличие в том, что в Scheme макросы по умолчанию гигиеничны (но гигиену можно обойти, если очень хочется), а в Lisp - нет.
R: Да, но тут метод добивания совершенно иной! И весь процесс добивания - это какой-то костыль “получите те же результаты, но контринтуитивно”.
Note(Mary): Зато в других макросах всё не сломается внезапно от того, что где в скоупе оказалась переменная с неверным именем.
R:Ну не так то сложно использовать gensym и не засорять чужую область видимости. И дальше там будет о макросах которые это делают еще проще
Note(Mary): Не сложно. Вопрос только в дефолтовом поведении.
Gensym создаёт выводимое имя и создаёт новый символ с этим именем. Она возвращает новый неинтернированный символ.
Созданное имя содержит префикс (по-умолчанию G), с последующим десятичным представлением числа.
Gensym обычно используется для создания символа, который не виден пользователю, и его имя не имеет важности. Необязательный аргумент используется нечасто. Имя образовано от «генерация символа», и символы созданные, таким образом, часто называются «gensyms».
(defmacro swap (pl1 pl2)
"Macro to swap two places"
(let ((temp1-name (gensym))
(temp2-name (gensym)))
`(let ((,temp1-name ,pl1)
(,temp2-name ,pl2))
(setf ,pl1 ,temp2-name)
(setf ,pl2 ,temp1-name))))
(defparameter *var1* 123)
(defparameter *var2* 456)
(swap *var1* *var2*)
*var1*
=>456
*var1*
=>123
Если необходимо, чтобы сгенерированные символы были интернированными и отличными от существующих символов, тогда удобно использовать функцию gentemp.
Gentemp, как и gensym, создаёт и возвращает новый символ. gentemp отличается от gensym в том, что возвращает интернированный символ в пакете package. Gentemp гарантирует, что символ будет новым, и не существовал ранее в указанном пакете. Она также использует счётчик, однако если полученный символ уже существует счётчик наращивается, и действия повторяются, пока не будет найдено имя ещё не существующего символа. Сбросить счётчик невозможно. Кроме того, префикс для gentemp не сохраняется между вызовами. Если аргумент prefix опущен, то используется значение по-умолчанию T.
Macro-Writing Macros (http://www.gigamonkeys.com/book/macros-defining-your-own.html)
Of course, there’s no reason you should be able to take advantage of macros only when writing functions. The job of macros is to abstract away common syntactic patterns, and certain patterns come up again and again in writing macros that can also benefit from being abstracted away.
In fact, you’ve already seen one such pattern–many macros will, like the last version of do-primes, start with a LET that introduces a few variables holding gensymed symbols to be used in the macro’s expansion. Since this is such a common pattern, why not abstract it away with its own macro?
In this section you’ll write a macro, with-gensyms, that does just that. In other words, you’ll write a macro-writing macro: a macro that generates code that generates code. While complex macro-writing macros can be a bit confusing until you get used to keeping the various levels of code clear in your mind, with-gensyms is fairly straightforward and will serve as a useful but not too strenuous mental limbering exercise.
You want to be able to write something like this:
(defmacro do-primes ((var start end) &body body)
(with-gensyms (ending-value-name)
`(do ((,var (next-prime ,start) (next-prime (1+ ,var)))
(,ending-value-name ,end))
((> ,var ,ending-value-name))
,@body)))
and have it be equivalent to the previous version of do-primes. In other words, the with-gensyms needs to expand into a LET that binds each named variable, ending-value-name in this case, to a gensymed symbol. That’s easy enough to write with a simple backquote template.
(defmacro with-gensyms ((&rest names) &body body)
`(let ,(loop for n in names collect `(,n (gensym)))
,@body))
Note how you can use a comma to interpolate the value of the LOOP expression. The loop generates a list of binding forms where each binding form consists of a list containing one of the names given to with-gensyms and the literal code (gensym). You can test what code the LOOP expression would generate at the REPL by replacing names with a list of symbols.
CL-USER> (loop for n in '(a b c) collect `(,n (gensym)))
((A (GENSYM)) (B (GENSYM)) (C (GENSYM)))
After the list of binding forms, the body argument to with-gensyms is spliced in as the body of the LET. Thus, in the code you wrap in a with-gensyms you can refer to any of the variables named in the list of variables passed to with-gensyms.
If you macro-expand the with-gensyms form in the new definition of do-primes, you should see something like this:
(let ((ending-value-name (gensym)))
`(do ((,var (next-prime ,start) (next-prime (1+ ,var)))
(,ending-value-name ,end))
((> ,var ,ending-value-name))
,@body))
Looks good. While this macro is fairly trivial, it’s important to keep clear about when the different macros are expanded: when you compile the DEFMACRO of do-primes, the with-gensyms form is expanded into the code just shown and compiled. Thus, the compiled version of do-primes is just the same as if you had written the outer LET by hand. When you compile a function that uses do-primes, the code generated by with-gensyms runs generating the do-primes expansion, but with-gensyms itself isn’t needed to compile a do-primes form since it has already been expanded, back when do-primes was compiled. Another classic macro-writing MACRO: ONCE-ONLY
Another classic macro-writing macro is once-only, which is used to generate code that evaluates certain macro arguments once only and in a particular order. Using once-only, you could write do-primes almost as simply as the original leaky version, like this:
(defmacro do-primes ((var start end) &body body)
(once-only (start end)
`(do ((,var (next-prime ,start) (next-prime (1+ ,var))))
((> ,var ,end))
,@body)))
(macroexpand-1 '(do-primes (variable 1 20)
(print variable)))
However, the implementation of once-only is a bit too involved for a blow-by-blow explanation, as it relies on multiple levels of backquoting and unquoting. If you really want to sharpen your macro chops, you can try to figure out how it works. It looks like this:
(defmacro once-only ((&rest names) &body body)
(let ((gensyms (loop for n in names collect (gensym))))
`(let (,@(loop for g in gensyms collect `(,g (gensym))))
`(let (,,@(loop for g in gensyms for n in names collect ``(,,g
,,n)))
,(let (,@(loop for n in names for g in gensyms collect `(,n
,g)))
,@body)))))
(macroexpand-1 '(once-only (start end)
(print 123)))
=>
(LET ((#:G860 (GENSYM)) (#:G861 (GENSYM)))
`(LET ((,#:G860 ,START) (,#:G861 ,END))
,(LET ((START #:G860) (END #:G861))
(PRINT 123)))), T
Или немного о том, как плохие люди, собрались вместе и испортили интернет :) По мотивам статьи http://habrahabr.ru/post/269565/ и того факта, что javascript раньше был схемой
Q(Mary): А причём тут эта статья? Там ни слова про JS.
В твоем языке есть макросы, но
- Нет строковой интерполяции - у тебя есть строковая интерполяция благодаря макросам
- Нет try-with-resources/using - у тебя есть using благодаря макросам
- Нет yield return - у тебя есть yield return благодаря макросам
- Нет нормального async/await, но есть дурацкие коллбеки - у тебя есть async/await благодаря макросам.
- Нет возможности определить тип по твоему специфичному шаблону (и наследование не решает проблему) - у тебя есть такая возможность благодаря макросам.
- Нет паттерн матчинга - есть паттерн матчинг благодаря макросам.
- В языке нет статической проверки типов? Можно скрутить себя в ежа и сделать поддержку статической проверки типов с крутым автовыводом.
- Ты хочешь описывать решения своих задач максимально выразительно? Да здравствуют макросы.
(не уверен осилю ли я это и осилит ли это кто-нибудь понять)
Q(Mary): а есть ли пример того, когда это действительно нужно?
Честно говоря - нет. Но если ты пишешь “Самоходное программное обеспечение” - то это может пригодиться… Или вот пример - анализ обфусцированного кода, или автоматическое написание виртуализированной среды по коду-источнику - все, в общем, весьма специфичные идеи :)
Note(Mary): Оставь это на будущие доклады.
(галопом, без погружения)
Note(Mary): ИМХО, нет смысла их различать, второе - прямой потомок первого. Окей :)
http://eax.me/avoid-metaprogramming/
В любой команде рано или поздно появляется человек, который совсем недавно прочитал книжку по Lisp или осилил Template Haskell и потому ему не терпится применить метапрограммирование на практике. Однако проблема заключается в том, что в большинстве случаев макросы или шаблоны создают больше проблем, чем решают. В этой заметке будет показано, почему так.
Примечание: Далее под макросами и шаблонами я буду иметь ввиду средства метапрограммирования, не привязанные к конкретному языку. Это могут быть макросы в Clojure или Scala, шаблоны в Haskell, parse_transform и, опять таки, макросы в Erlang и так далее. Все эти средства примерно об одном и том же.
- Вообще, трудно представить или даже специально придумать задачу,
которую невозможно решить без макросов. Мне в голову приходило
использовать макросы 1-2 раза, и то задачу можно было решить
иначе. Почти всегда проблему на самом деле можно решить обычными
средствами языка.
- Трудно придумать задачу, которую невозможно решить без акторов (генераторов, FSM, regex, smth_feature)
- Допустим, с помощью макросов вы хотите получить некую информацию при компиляции приложения. Например, вы генерируете версию приложения на основе тэгов в репозитории. Но то же самое можно сделать просто дописав пару строк в Makefile. Притом не обязательно указывать версию приложения в коде. Почему бы не указать ее в конфиге?
- Никто не отменял обычную кодогенерацию. Например, Thrift,
Protobuf и прочие делают в сущности то же самое, что вы хотите от
макросов. Если же такие решения плохо интегрируются с вашей
системой сборки или IDE, возможно, просто у вас фиговая система
сборки или IDE.
- Макросы предпочтительнее (удобнее и лучше) внешней кодогенерации. KISS!
- При нормальной поддержке макросов я увижу нормальные исключения (с “неразвёрнутыми” макросами на положенных местах, например)
- Допустим, код, полученный с помощью шаблонов или макросов, бросит
исключение. Какие номера строк вы увидите в стэктрейсе? Сможете
ли вы легко разобраться в проблеме и исправить ее, особенно, если
проблемный код написан кем-то другим?
- Проблемы индейцев
- Если вы используете шаблоны, то скорее всего сломаете мозг своей
любимой IDE. Допустим, шаблон генерирует новые функции. Чтобы
узнать о них, IDE нужно иметь встроенный интерпретатор вашего
языка программирования. Скорее всего, она его не имеет, а значит
будет подчеркивать сгенерированные функции красным, когда вы
будете пытаться их использовать, так как в коде их как бы и нет.
- Проблемы индейцев
- IDE может держать запущенным REPL и использовать для интроспекции его (в Lisp-языках это так и делается).
- Возможно, вы пытаетесь замаскировать при помощи шаблонов дублирование кода. Есть менее радикальные способы борьбы с code smell. Дублированный код почти всегда можно вынести в отдельные методы. Или параметризовать различающиеся части, передав лямбду.
- Большинство программистов просто не умеют работать с макросами и
шаблонами. Помните, что вы не одни в команде. Даже если ваши
коллеги знают макросы, все равно их использование существенно
усложнит понимание и поддержку кода.
- это именно для того, чтобы писать макросы, их использование никаких особых навыков/знаний не требует.
- каждый разработчик должен иметь начальное представление о макросах, или его не нужно брать в команду
- Зачастую макросы могут быть причиной странных и непонятных
ошибок. Например, в Erlang вы можете собрать beam с какими-то
макросами, затем обновить зависимость, в которой объявлен этот
макрос, и собрать остальную часть проекта уже с другим
макросом. Попробуйте потом отладить ошибки, которые посыпятся!
Или, допустим, макрос использует текущее время. Вы один раз
скомпилировали код, он закэшировался. Затем вы пересобираете
проект и не понимаете, почему время не изменяется.
- Проблемы индейцев
- Нередко макросы — это нестабильная и вообще экспериментальная
фича языка. В Scala и так все постоянно меняется, а вы еще
предлагаете взять макросы. В Template Haskell тоже вон все
поменялось, теперь там есть какие-то типизированные макросы. Плюс
к этому в реализации макросов нередко имеются какие-то мелкие
косяки и неудобства. Так в Template Haskell шаблон не может быть
объявлен и использован в одном файле.
- Проблемы индейцев
- Вы изобретаете кучу различных DSL. Каждый язык при этом, понятное
дело, отличается от других. Почему бы просто не писать на одном
языке? Может быть, он просто недостаточно выразителен и следует
найти язык получше?
- Да, DSL под задачу - это и есть то, к чему нужно стремиться!
http://swizard.info/articles/functional-data-structures.html http://swizard.livejournal.com/157521.html http://habrahabr.ru/post/143490/ http://fprog.ru/2010/issue5/vsevolod-dyomkin-lisp-philosophy/ http://rus-linux.net/MyLDP/algol/LISP/lisp09.html http://lisp2d.net/rus/teach/q.php http://cyberleninka.ru/article/n/analiz-vozmozhnostey-sistemy-makroopredeleniy-yazyka-common-lisp-dlya-sozdaniya-novyh-upravlyayuschih-konstruktsiy https://books.google.ru/books?id=jaoDX9-e_McC&pg=PA738&lpg=PA738&dq=%D0%BF%D1%80%D0%B8%D0%BC%D0%B5%D1%80+%D0%BC%D0%B0%D0%BA%D1%80%D0%BE%D1%81+%D0%BB%D0%B8%D1%81%D0%BF&source=bl&ots=lHfSI5iKbj&sig=sLvsICw1e7p9ehee3zMpKHqT6Kk&hl=ru&sa=X&ved=0CFkQ6AEwCTgUahUKEwidzOn12eDIAhXK_3IKHdliDm8#v=onepage&q=%D0%BF%D1%80%D0%B8%D0%BC%D0%B5%D1%80%20%D0%BC%D0%B0%D0%BA%D1%80%D0%BE%D1%81%20%D0%BB%D0%B8%D1%81%D0%BF&f=false http://linux.yaroslavl.ru/docs/prog/m4.html
http://cl-cookbook.sourceforge.net/macros.html http://www.linux.org.ru/forum/development/9708251 https://psg.com/~dlamkins/sl/chapter20.html http://community.schemewiki.org/?hygiene-versus-gensym http://letoverlambda.com/index.cl/toc http://www.aaronsw.com/weblog/pgwrong http://www.randomhacks.net/2002/09/13/hygienic-macros/