Я пишу Lisp в Haskell (код в GitHub) как способ узнать больше об обоих языках.
Новейшая функция, которую я добавляю, - это макросы. Не гигиенические макросы или что-то необычное - простые преобразования ванильного кода. В моей первоначальной реализации была отдельная макросреда, отличная от среды, в которой живут все другие значения. Внутри функций read
и eval
я вставлял другую функцию macroExpand
, которая проходила дерево кодов и выполняла соответствующие преобразования всякий раз он нашел ключевое слово в макросреде, до того, как окончательная форма была передана на eval
для оценки. Хорошим преимуществом этого было то, что макросы имели такое же внутреннее представление, что и другие функции, что уменьшало дублирование кода.
Если две среды казались неуклюжими, и это раздражало меня, что, если бы я хотел загрузить файл, eval
должен был иметь доступ к среде макроса, если файл содержит определения макросов. Поэтому я решил ввести макрос, сохранить макросы в той же среде, что и функции и переменные, и включить фазу макроразложения в eval
. Сначала я был немного потерял, как это сделать, пока не понял, что могу просто написать этот код:
eval env (List (function : args)) = do
func <- eval env function
case func of
(Macro {}) -> apply func args >>= eval env
_ -> mapM (eval env) args >>= apply func
Он работает следующим образом:
- Если вам передан список, содержащий начальное выражение и кучу других выражений...
- Оцените первое выражение
- Если это макрос, затем примените его к аргументам и оцените результат
- Если это не макрос, затем оцените аргументы и примените функцию к результату
Как будто макросы точно такие же, как функции, за исключением того, что порядок eval/apply переключается.
Это точное описание макросов? Не хватает ли чего-то важного, реализуя макросы таким образом? Если ответы "да" и "нет", то почему я раньше не видел, чтобы макросы объясняли это раньше?