Как мне написать этот макрос без использования `eval`? - PullRequest
3 голосов
/ 15 марта 2019

Я пытаюсь написать макрос, который создает произвольное количество вложенных циклов и выполняет некоторый код на каждой итерации циклов. В моей первой попытке (показанной ниже) макрос возвратил код вместо того, чтобы запустить его.

;; WRONG! Returns a bunch of nested loops instead of evaluating the code.

(defmacro do-combinations ((var lists) &body body)
  `(let* ((lst (mapcar #'(lambda (x)
                           `(loop for ,(gensym) in (list ,@x) do))
                       ,lists))
          (symbols (mapcar #'caddr lst)))
     (reduce #'(lambda (x y) `(,@y ,x))
             lst
             :initial-value `(let ((,',var (list ,@symbols)))
                               (progn ,',@body)))))
CL-USER 25 : 1 > (do-combinations (n '((1 2 3)
                                       (10 20 30)
                                       (100 200 300)))
                   (pprint n))
(LOOP FOR #:G872 IN (LIST 100 200 300)
      DO (LOOP FOR #:G871 IN (LIST 10 20 30)
               DO (LOOP FOR #:G870 IN (LIST 1 2 3)
                        DO (LET # #))))

Моим последним прибежищем для этого было вставлено eval

;; Ugly fix with eval
(defmacro do-combinations ((var lists) &body body)
  `(let* ((lst (mapcar #'(lambda (x)
                           `(loop for ,(gensym) in (list ,@x) do))
                       ,lists))
          (symbols (mapcar #'caddr lst)))
     (eval (reduce #'(lambda (x y) `(,@y ,x))
                   lst
                   :initial-value `(let ((,',var (list ,@symbols)))
                                     (progn ,',@body))))))    

CL-USER 35 : 1 > (do-combinations (n '((1 2 3)
                                       (10 20 30)
                                       (100 200 300)))
                   (pprint n))

(1 10 100)
(2 10 100)
...

Исправление работает (вроде), но выглядит ужасно. Как бы вы написали этот макрос более элегантно, не прибегая к eval?

1 Ответ

9 голосов
/ 15 марта 2019

Существует множество основных проблем (например, какой код должен быть сгенерирован и когда) в уже слегка сложном макросе. Сначала вы можете подумать о более простых примерах макросов. Но можно заставить ваш код работать, поэтому не все потеряно.

Давайте рассмотрим некоторые проблемы:

Как использовать макрос в коде

Вы хотите использовать свой макрос следующим образом:

(do-combinations (n '((1 2 3)
                      (10 20 30)
                      (100 200 300)))
  (pprint n))

Но нет смысла цитировать вложенный список. Макрос генерирует код, вероятно, во время компиляции, и в то время список должен быть известен. Таким образом, вы не можете или не можете это оценить. Таким образом можно удалить цитату:

(do-combinations (n ((1 2 3)
                     (10 20 30)
                     (100 200 300)))
  (pprint n))

Некоторые основы макросов

Теперь, когда вы пишете макрос, нужно понимать следующие основные вещи:

  • макрос сгенерирует код. Вы должны знать, какой код должен генерировать ваш макрос. Запишите код и сравните его с тем, что делает ваш макрос.
  • чтобы увидеть, что генерирует макрос, используйте macroexpand и macroexpand-1. Используйте pprint, чтобы красиво напечатать полученный код.

Давайте посмотрим на генерируемый код

Теперь давайте посмотрим на код, который генерирует ваш макрос:

CL-USER 145 > (pprint
               (macroexpand-1 '(do-combinations (n ((1 2 3)
                                                    (10 20 30)
                                                    (100 200 300)))
                                 (pprint n))))

(LET* ((LST
        (MAPCAR #'(LAMBDA (X) `(LOOP FOR ,(GENSYM) IN (LIST ,@X) DO))
                ((1 2 3) (10 20 30) (100 200 300))))
       (SYMBOLS (MAPCAR #'CADDR LST)))
  (REDUCE #'(LAMBDA (X Y) `(,@Y ,X))
          LST
          :INITIAL-VALUE
          `(LET ((N (LIST ,@SYMBOLS))) (PROGN (PPRINT N)))))

Вы можете видеть, что все это неправильно, поскольку генерируется много кода, который должен выполняться во время расширения макроса, а не во время выполнения! Это вовсе не генерация вложенных циклов.

Вы можете увидеть в своем макросе эту вторую строку:

`(let* ((lst (mapcar #'(lambda (x)

Это означает, что код будет сгенерирован. Но вы, вероятно, хотите запустить его в фазе расширения.

Лучшая версия

Вот версия с правильным генерированием кода:

(defmacro do-combinations ((var lists) &body body)
  (let* ((lst (mapcar #'(lambda (x)
                           `(loop for ,(gensym) in (list ,@x) do))
                       lists))
         (symbols (mapcar #'caddr lst)))
     (reduce #'(lambda (x y) `(,@y ,x))
             lst
             :initial-value `(let ((,var (list ,@symbols)))
                               ,@body))))

Посмотрим:

CL-USER 147 > (pprint
               (macroexpand-1 '(do-combinations (n ((1 2 3)
                                                    (10 20 30)
                                                    (100 200 300)))
                                 (pprint n))))

(LOOP FOR #:G424120 IN (LIST 100 200 300)
      DO (LOOP FOR #:G424119 IN (LIST 10 20 30)
               DO (LOOP FOR #:G424118 IN (LIST 1 2 3)
                        DO (LET ((N (LIST #:G424118 #:G424119 #:G424120)))
                             (PPRINT N)))))
...