Я изучаю (общий) Лисп, и в качестве упражнения я хочу реализовать «xond», макрос cond
, который преобразует этот глупый пример:
(xond (= n 1) (setq x 2) (= n 2) (setq x 1))
в цепочку if-else:
(if (= n 1) (setq x 2) (if (= n 2) (setq x 1)))
В настоящее время у меня есть этот макрос:
(defmacro xond (&rest x) (if x (list 'progn (list 'if (pop x) (pop x)))))
которые просто расширяют первые два элемента в x
:
(macroexpand '(xond (= x 1) (setq y 2)))
производить
(PROGN (IF (= X 1) (SETQ Y 2))) ;
Теперь я хочу обработать все элементы в x
, поэтому я добавляю loop
для создания серии if (шаг к версии if-else):
(defmacro xond (&rest x)
(loop (if x
(list 'progn (list 'if (pop x) (pop x)))
(return t))))
но затем макрос, похоже, перестает работать:
(macroexpand '(xond (= x 1) (setq y 2)))
T ;
Что мне здесь не хватает?
Версия
Ответ verdammelt поставил меня на правильный путь, а coredump заставил меня изменить свой подход на итеративный.
Теперь я реализую (xond test1 exp1 test2 exp2)
как:
(block nil
test1 (return exp1)
test2 (return exp2)
)
что можно сделать итерацией.
Я пишу это для своего минимального интерпретатора Лиспа; Я реализовал только самые основные функции.
Это то, что я написал. Я использую la
для накопления частей вывода.
(defmacro xond (&rest x)
(let ((la '()))
(loop
(if x (push (list 'if (pop x) (list 'return (pop x))) la)
(progn (push 'nil la)
(push 'block la)
(return la)
)))))
с
(macroexpand '(xond (= x 1) (setq y 2) (= X 2) (setq y 1)))
результат:
(BLOCK NIL
(IF (= X 2) (RETURN (SETQ Y 1)))
(IF (= X 1) (RETURN (SETQ Y 2)))
) ;
Второе издание
Добавьте метку к block
и измените return
на return-from
, чтобы избежать конфликта с другими return
внутренними аргументами. Также изменен push
на append
для генерации кода в том же порядке, что и параметры.
(defmacro xond (&rest x)
(let ((label (gensym)) (la '()) (condition nil) (expresion nil))
(setq la (append la (list 'block label)))
(loop
(if x
(setq la (append la (list
(list 'if (pop x) (list 'return-from label (pop x))))))
(return la)))))
Так
(macroexpand '(xond (= x 1) (setq y 2) (= X 2) (setq y 1)))
теперь дает
(BLOCK #:G3187 (IF (= X 1) (RETURN-FROM #:G3187 (SETQ Y 2))) (IF (= X 2) (RETURN-FROM #:G3187 (SETQ Y 1))))
@coredump. Я отредактировал свой вопрос, чтобы добавить возврат, как было предложено.
Ваш (block nil ... (return ...))
подход не гигиеничен: учтите (loop ... (xond ... (return 3)) ...)
. Вам определенно не нужно использовать return
/ return-from
в таком макросе, но если вы это делаете, делайте это гигиенично, используя gensym для имени блока.
@tfb. Только что отредактировал мой вопрос с вашими комментариями. Я хочу использовать «возврат», чтобы пропустить оставшиеся условия.
Вам не нужно этого делать, если вы выберете очевидный подход (как рекомендуется в ответе coredump) создания (if c1 r1 (if c2 r2 ...))
. Это гораздо более естественно для языка выражений вроде Лиспа, чем какие-то дерьмовые императивные вещи с явной передачей управления повсюду.
@tfb. Я это понимаю, но для этого нужно несколько еще не реализованных функций в моем минимальном интерпретаторе Лиспа.
@CandidMoe: нужно работающее макрорасширение, вот и все.
Ваш макрос xond
заканчивается на (return t)
, поэтому он оценивается как t
, а не как ваши накопленные if
выражения.
Вы можете использовать предложение loop
collect
для накопления кода, который вы хотите вернуть. Например: (loop for x in '(1 2 3) collect (* 2 x))
будет оцениваться как (2 4 6)
.
Я вижу это сейчас. Я понимаю проблему; в Python я бы собирал строки в список и возвращал join
конец, но здесь я потерялся. Код будет оценен.
Если у меня будет время посвятить себя этой проблеме, я посмотрю, смогу ли я сделать что-нибудь, чтобы помочь вам. А пока я добавил предложение о пункте collect
в loop
, которое, вероятно, было бы здесь полезно.
Вы отвечаете, дайте подсказку, чтобы решить проблему. Я изучаю Лисп и хочу решить его с помощью самых простых инструментов.
Некоторые замечания
progn
, когда вы расширяетесь только до одного if
pop
может сбить с толку читателя (и программиста тоже), поскольку оно мутирует место, возможно, вы захотите начать с менее императивного подхода.Кроме того, в этом случае я не думаю, что подход loop
полезен, потому что вам нужно вложить выражения, которые следуют в теле, внутри ранее созданной формы, и хотя это можно сделать, это немного сложнее. сделайте это просто рекурсивной функцией или "рекурсивным" макросом.
Здесь я объясняю оба подхода, начиная с "рекурсивного" макроса (цитата здесь потому, что макрос не вызывает сам себя, а расширяется как вызов самого себя).
Если бы мне пришлось реализовать xond
, я бы написал макрос, который расширяется в другие вызовы xond
, пока макрорасширение не достигнет базового случая, когда больше нет xond
:
(defmacro xond (&rest body)
(if (rest body)
(destructuring-bind (test if-action . rest) body
`(if ,test ,if-action (xond ,@rest)))
(first body)))
Например, это выражение:
(xond (= n 1) (setq x 2) (= n 2) (setq x 1))
Первый макрос раскрывается в:
(if (= n 1)
(setq x 2)
(xond (= n 2) (setq x 1)))
И в конечном итоге достигает фиксированной точки с:
(if (= n 1)
(setq x 2)
(if (= n 2)
(setq x 1)
nil))
Будьте осторожны, вы не можете напрямую использовать xond
внутри определения xond
, происходит то, что макрос расширяется как вызов xond
, который Lisp затем расширяет снова. Если вы не будете осторожны, вы можете получить бесконечное макрорасширение, поэтому вам нужен базовый случай, когда макрос не расширяется в xond
.
В качестве альтернативы вы можете вызвать рекурсивную функцию внутри вашего макроса и развернуть все внутренние формы одновременно.
С помощью LABELS вы привязываете xond-expand
к рекурсивной функции. Вот это реальный рекурсивный подход:
(labels ((xond-expand (body)
(if body
(list 'if
(pop body)
(pop body)
(xond-expand body))
nil)))
(xond-expand '((= n 1) (setq x 2) (= n 2) (setq x 1))))
; => (IF (= N 1)
; (SETQ X 2)
; (IF (= N 2)
; (SETQ X 1)
; NIL))
В зависимости от того, для чего предназначен xond
, может потребоваться случай ошибки для длины списка аргументов, равной 1, я думаю (она должна быть равна нулю, 2 или более).
@tfb в макросе destructuring-bind приведет к ошибке, но я согласен, что обработка ошибок должна быть лучше. В метках я тоже перепишу, чтобы использовать поп, так что тоже будет ошибка.
@tfb Я не ожидаю обработки ошибок в ответе. Это можно добавить позже.
Как насчет
(ql:quickload :alexandria)
(defun as-last (l1 l2)
`(,@l1 ,l2))
(defmacro xond (&rest args)
(reduce #'as-last
(loop for (condition . branch) in (alexandria:plist-alist args)
collect `(if ,condition ,branch))
:from-end t))
(macroexpand-1 '(xond c1 b1 c2 b2 c3 b3))
;; (IF C1 B1 (IF C2 B2 (IF C3 B3))) ;
;; T
alexandria
plist-alist
использовался для объединения аргументов,
внутренняя деструктуризация в loop
используется для извлечения условий и ветвей.
Вспомогательная функция as-last
складывает списки вместе в виде
(a b c) (d e f) => (a b c (d e f))
.
(reduce ... :from-end t)
сворачивает вправо последовательность собранных (if condition branch)
предложений, складывая их друг в друга с помощью #'as-last
.
(«хотя, alexandria
вообще считается зависимостью? ;))
(defun pairs (l &key (acc '()) (fill-with-nil-p nil))
(cond ((null l) (nreverse acc))
((null (cdr l)) (pairs (cdr l)
:acc (cons (if fill-with-nil-p
(list (car l) nil)
l)
acc)
:fill-with-nil-p fill-with-nil-p))
(t (pairs (cdr (cdr l))
:acc (cons (list (car l) (cadr l)) acc)
:fill-with-nil-p fill-with-nil-p))))
(defun as-last (l1 l2)
`(,@l1 ,l2))
(defmacro xond (&rest args)
(reduce #'as-last
(loop for (condition branch) in (pairs args)
collect `(if ,condition ,branch))
:from-end t))
(macroexpand-1 '(xond c1 b1 c2 b2 c3 b3))
;; (IF C1 B1 (IF C2 B2 (IF C3 B3))) ;
;; T
Вспомогательная функция pairs
делает из (a b c d e f)
=> ((a b) (c d) (e f))
.
(:fill-with-nil-p
определяет в случае нечетного числа элементов списка, будет ли последний элемент в списке (last-el)
или (last-el nil)
- в последнем случае заполнен nil
).
Редактирование вводит другое расширение, которое имеет другую семантику, т.е. Если несколько тестов верны. Может быть, вы хотите добавить пункт возврата?