Проверьте расширение +=
, чтобы найти ошибку
Вам необходимо проверить расширение:
CL-USER 3 > (defmacro += (x y)
(list 'setf x '(+ x y)))
+=
CL-USER 4 > (macroexpand-1 '(+= a 1))
(SETF A (+ X Y))
T
Расширение макроса выше показывает, что используются x
и y
, что является ошибкой.
Нам нужно оценить их внутри функции макроса:
CL-USER 5 > (defmacro += (x y)
(list 'setf x (list '+ x y)))
+=
CL-USER 6 > (macroexpand-1 '(+= a 1))
(SETF A (+ A 1))
T
Выше выглядит лучше. Примечание, кстати. что макрос уже существует в стандартном Common Lisp. Это называется incf
.
Обратите внимание, что вам это не нужно, потому что побочный эффект не требуется в вашем коде iterate
. Мы можем просто использовать функцию +
без установки какой-либо переменной.
Style
Возможно, вы захотите настроить немного под стиль Лисп:
- no camelCase -> считыватель по умолчанию не учитывает регистр символов
- говорящих имен переменных -> улучшает читаемость
- строка документации в макросе / функции - улучшает читаемость
GENSYM
принимает строку аргумента -> улучшает читаемость сгенерированного кода
- без висячих скобок и без пробелов между скобками -> делает код более компактным
- лучше и автоматический отступ -> улучшает читаемость
- тело помечено
&body
, а не &rest
-> улучшает автоматическое отступление макрокоманд с помощью iterate
do
не требуется макрос +=
для обновления итерационной переменной, поскольку do
обновляет саму переменную -> побочных эффектов не требуется, нам нужно только вычислить следующее значение
- Как правило, для написания хорошего макроса требуется немного больше времени, чем для написания нормальной функции, потому что мы программируем на мета-уровне с генерацией кода, и есть еще о чем подумать и несколько основных подводных камней. Итак, не торопитесь, перечитайте код, проверьте расширения, напишите некоторую документацию, ...
Применительно к вашему коду оно теперь выглядит так:
(defmacro iterate (variable start end step &body body)
"Iterates VARIABLE from START to END by STEP.
For each step the BODY gets executed."
(let ((end-variable (gensym "END"))
(step-variable (gensym "STEP")))
`(do ((,variable ,start (+ ,variable ,step-variable))
(,end-variable ,end)
(,step-variable ,step))
((> ,variable ,end-variable) t)
,@body)))
В Лиспе первая часть - переменная, начало, конец, шаг - обычно записывается в виде списка. См. Например DOTIMES
. Это позволяет, например, сделать step
необязательным и присвоить ему значение по умолчанию:
(defmacro iterate ((variable start end &optional (step 1)) &body body)
"Iterates VARIABLE from START to END by STEP.
For each step the BODY gets executed."
(let ((end-variable (gensym "END"))
(step-variable (gensym "STEP")))
`(do ((,variable ,start (+ ,variable ,step-variable))
(,end-variable ,end)
(,step-variable ,step))
((> ,variable ,end-variable) t)
,@body)))
Посмотрим расширение, отформатированное для удобства чтения. Мы используем функцию macroexpand-1
, которая выполняет расширение макроса только один раз, а не расширение макроса сгенерированного кода.
CL-USER 10 > (macroexpand-1 '(iterate (i 1 10 2)
(print i)
(print (* i 2))))
(DO ((I 1 (+ I #:STEP2864))
(#:END2863 10)
(#:STEP2864 2))
((> I #:END2863) T)
(PRINT I)
(PRINT (* I 2)))
T
Вы можете видеть, что символы, созданные gensym
, также могут быть идентифицированы по их имени.
Мы также можем позволить Лиспу отформатировать сгенерированный код, используя функцию pprint
и указав правильное поле.
CL-USER 18 > (let ((*print-right-margin* 40))
(pprint
(macroexpand-1
'(iterate (i 1 10 2)
(print i)
(print (* i 2))))))
(DO ((I 1 (+ I #:STEP2905))
(#:END2904 10)
(#:STEP2905 2))
((> I #:END2904) T)
(PRINT I)
(PRINT (* I 2)))