Использование цикла внутри defmacro

Я изучаю (общий) Лисп, и в качестве упражнения я хочу реализовать «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 26.12.2020 20:13

@coredump. Я отредактировал свой вопрос, чтобы добавить возврат, как было предложено.

Candid Moe 26.12.2020 20:20

Ваш (block nil ... (return ...)) подход не гигиеничен: учтите (loop ... (xond ... (return 3)) ...). Вам определенно не нужно использовать return / return-from в таком макросе, но если вы это делаете, делайте это гигиенично, используя gensym для имени блока.

user5920214 27.12.2020 12:05

@tfb. Только что отредактировал мой вопрос с вашими комментариями. Я хочу использовать «возврат», чтобы пропустить оставшиеся условия.

Candid Moe 27.12.2020 14:47

Вам не нужно этого делать, если вы выберете очевидный подход (как рекомендуется в ответе coredump) создания (if c1 r1 (if c2 r2 ...)). Это гораздо более естественно для языка выражений вроде Лиспа, чем какие-то дерьмовые императивные вещи с явной передачей управления повсюду.

user5920214 27.12.2020 18:54

@tfb. Я это понимаю, но для этого нужно несколько еще не реализованных функций в моем минимальном интерпретаторе Лиспа.

Candid Moe 27.12.2020 19:50

@CandidMoe: нужно работающее макрорасширение, вот и все.

user5920214 27.12.2020 20:27
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
7
201
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ваш макрос xond заканчивается на (return t), поэтому он оценивается как t, а не как ваши накопленные if выражения.

Вы можете использовать предложение loopcollect для накопления кода, который вы хотите вернуть. Например: (loop for x in '(1 2 3) collect (* 2 x)) будет оцениваться как (2 4 6).

Я вижу это сейчас. Я понимаю проблему; в Python я бы собирал строки в список и возвращал join конец, но здесь я потерялся. Код будет оценен.

Candid Moe 26.12.2020 15:59

Если у меня будет время посвятить себя этой проблеме, я посмотрю, смогу ли я сделать что-нибудь, чтобы помочь вам. А пока я добавил предложение о пункте collect в loop, которое, вероятно, было бы здесь полезно.

verdammelt 26.12.2020 16:42

Вы отвечаете, дайте подсказку, чтобы решить проблему. Я изучаю Лисп и хочу решить его с помощью самых простых инструментов.

Candid Moe 26.12.2020 16:53
Ответ принят как подходящий

Некоторые замечания

  • Вам не нужен 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 или более).

user5920214 26.12.2020 19:49

@tfb в макросе destructuring-bind приведет к ошибке, но я согласен, что обработка ошибок должна быть лучше. В метках я тоже перепишу, чтобы использовать поп, так что тоже будет ошибка.

coredump 26.12.2020 20:01

@tfb Я не ожидаю обработки ошибок в ответе. Это можно добавить позже.

Candid Moe 26.12.2020 22:41

Как насчет

(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

alexandriaplist-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).

Другие вопросы по теме

Похожие вопросы