Другие ответы уже указали правильное решение: использовать PROGN.
Вот несколько замечаний о «стиле»:
(defmacro N_defheight(_oct _note _offset)
`(defconstant ,(read-from-string (concatenate 'string _note _oct))
,(+ (eval (read-from-string (concatenate 'string "N_oct" _oct)))
_offset)))
- Для чего нужны переменные с начальным подчеркиванием? Это не распространенная практика Lisp в Common Lisp
- READ-FROM-STRING может быть заменен на INTERN и STRING-UPCASE.
- Возможно, вы захотите контролировать пакет для символа, генерируемого INTERN (или READ-FOR-STRING).
- EVAL можно заменить на SYMBOL-VALUE
- CONCATENATE можно заменить на FORMAT: (формат nil "N-OCT ~ a" oct)
- для определяющего макроса, DEF должен быть началом имени. Это просто соглашение.
* * Пример тысяча двадцать-одиной: * * 1 022
(defmacro N_octave(_octave)
`(progn
(N_defheight ,_octave "c" 0)
...
(N_defheight ,_octave "h" 11))
Выше можно упростить с помощью простой итерации:
`(progn
,@(loop for (note offset) in '(("c" 0) ("c#" 1) ... ("h" 11))
collect (list 'defheight octave note offset)))
или используя MAPCAR
`(progn
,@(mapcar (lambda (desc)
(destructuring-bind (note offset) desc
(list 'defheight octave note offset)))
'(("c" 0) ("c#" 1) ... ("h" 11))))
Эффект меньше набирается, а важные символы пишутся только один раз.
Нужно решить, что лучше: много похожих выражений или небольшая программа, преобразующая описание данных.
Но есть еще одна проблема: данные закодированы в макрос.
Это неправильно. Макрос должен выполнять преобразование кода и не содержать данных. Опять же, вы можете делать все, но хороший Лисп требует некоторого чувства стиля программирования. Я бы поместил примечания и смещения в виде списка в переменную и использовал это в макросе, или предоставил бы его в качестве параметра:
(defvar *notes-and-offsets*
'(("c" 0) ("c#" 1) ... ("h" 11)))
(defoctave (octave notes-and-offsets)
`(progn
,@(mapcar (lambda (desc)
(destructuring-bind (note offset) desc
(list 'defheight octave note offset)))
(eval notes-and-offsets))))
(defoctave "0" *notes-and-offsets*)
Теперь есть еще одна проблема. Мы определяем константы с именами, такими как C0
. Константа в Лиспе всегда относится к глобальному постоянному значению. Переплет не допускается. Это означает, что C0 больше не является допустимым именем локальной переменной в вашей программе. Если вы знаете, что вы никогда не будете использовать C0 в качестве имени переменной, это нормально - но эта проблема может быть не известна позже во время обслуживания. По этой причине целесообразно ставить знаки плюс вокруг имен констант, например: +C0+
. Опять же, просто соглашение. Вы также можете использовать свое собственное специализированное соглашение об именах, которое не должно конфликтовать с вашими именами для переменных. как NOTE-C0
.
Если вы намерены всегда использовать идентификатор типа c0
в качестве глобального имени для значения постоянной ноты, то у вас нет проблем - вам просто нужно понять это, тогда с DEFCONSTANT вы не сможете использовать c0
больше не является переменной. Тогда, возможно, будет хорошей идеей иметь свой собственный пакет.
Далее: если вы хотите использовать переменные при вычислении расширения макроса, вам необходимо убедиться, что переменные имеют значения. Либо загрузите файл раньше, либо используйте EVAL-WHEN.
Это приводит к этому коду:
(eval-when (:compile-toplevel :load-toplevel :execute)
(defvar *n-oct0* 0)
(defvar *notes-and-offsets*
'((c 0) (c# 1) (des 1) (d 2)
(d# 3) (es 3) (e 4) (f 5)
(f# 6) (ges 6) (g 7) (g# 8)
(as 8) (a 9) (a# 10) (b 10)
(h 11)))
) ; end of EVAL-WHEN
(defmacro defheight (oct note offset)
`(defconstant ,(intern (format nil "~a~a" note oct))
(+ ,(intern (format nil "*N-OCT~a*" oct))
,offset)))
(defmacro defoctave (octave notes-and-offsets)
`(progn
,@(mapcar (lambda (note offset)
(list 'defheight octave note offset))
(mapcar #'first (eval notes-and-offsets))
(mapcar #'second (eval notes-and-offsets)))))
(defoctave 0 *notes-and-offsets*)