Что не так со следующим общим макросом Lisp с использованием gensym?

Обучение Common Lisp (с использованием GNU CLISP 2.43).. так что может быть ошибкой noob. Например, "печатные простые числа между x и y"

(defun is-prime (n)
  (if (< n 2) (return-from is-prime NIL))

  (do ((i 2 (1+ i)))
      ((= i n) T)
    (if (= (mod n i) 0) 
        (return NIL))))

(defun next-prime-after (n)
  (do ((i (1+ n) (1+ i)))
      ((is-prime i) i)))

(defmacro do-primes-v2 ((var start end) &body body)
  `(do ((,var (if (is-prime ,start)
                  ,start
                  (next-prime-after ,start))
              (next-prime-after ,var)))
       ((> ,var ,end))
     ,@body))

(defmacro do-primes-v3 ((var start end) &body body)
  (let ((loop-start (gensym))
        (loop-end (gensym))) 
    `(do ((,loop-start ,start)
          (,loop-end ,end)
          (,var (if (is-prime ,loop-start)
                    ,loop-start
                    (next-prime-after ,loop-start))
                (next-prime-after ,var)))
         ((> ,var ,loop-end))
       ,@body )))

do-primes-v2 отлично работает.

[13]> (do-primes-v2 (p 10 25) (format t "~d " p))
11 13 17 19 23

Далее я попытался использовать gensym, чтобы избежать именования столкновений при расширении макросов - do-primes-v3. Однако я застрял с

*** - EVAL: variable #:G3498 has no value

Пробовал использовать макро-расширение, чтобы увидеть, могу ли я обнаружить ошибку, но я не могу.

[16]> (macroexpand-1 `(do-primes-v3 (p 10 25) (format t "~d " p)))
(DO
 ((#:G3502 10) (#:G3503 25)
  (P (IF (IS-PRIME #:G3502) #:G3502 (NEXT-PRIME-AFTER #:G3502))
     (NEXT-PRIME-AFTER P)))
 ((> P #:G3503)) (FORMAT T "~d " P)) ;

Ответ 1

Используйте DO* вместо DO.

DO Инициализирует привязки в области, где они еще не видны. DO* инициализирует привязки в области видимости.

В этом конкретном случае var необходимо ссылаться на другое связывание loop-start.

Ответ 2

На самом деле вам не нужен gensym, чтобы избежать захвата переменной, потому что вы не вводите никаких переменных, которые были бы "локальными для макроса". Когда вы macroexpand ваш do-primes-v2, вы увидите, что переменная не введена, которая не существовала вне макроса.

Вам нужно это для другой вещи, хотя: избегайте нескольких оценок.

Если вы вызываете макрос следующим образом:

(do-primes-v2 (p (* x 2) (* y 3))
  (format "~a~%" p))

он расширяется до

(do ((p (if (is-prime (* x 2))
            (* x 2)
            (next-prime-after (* x 2))
        (next-prime-after p)))
    ((> p (* y 3))
  (format "~a~%" p))

В лучшем случае это неэффективно, потому что эти умножения выполняются несколько раз. Однако, если вы используете функцию с побочными эффектами в качестве входов, например setf или incf, это может быть большой проблемой.

Ответ 3

Переместите привязку вашего цикла и конца цикла к закрывающему блоку LET или используйте DO *. Причина в том, что все переменные цикла в DO связаны "параллельно", поэтому для первой привязки переменная (расширенная) loop-start еще не имеет привязки.

Ответ 4

Я знаю, что это не отвечает на ваш вопрос, но я считаю, что это актуально. По моему опыту, тип макроса, который вы пытаетесь написать, является очень распространенным. Одна из проблем, с которыми я столкнулась, связана с тем, что она не справляется с другим распространенным прецедентом: функциональная композиция.

У меня нет времени выделить некоторые из трудностей, с которыми вы, вероятно, столкнетесь, используя свой макрос, я, тем не менее, хочу подчеркнуть, что если бы вы построили ваш простой итератор, ориентированный на функциональную композицию, ваш макрос окажется чрезвычайно простым, избегая вашего вопроса вообще.

Примечание. Я немного изменил некоторые из ваших функций.

(defun is-prime (n)
  (cond
    ((< n 2)
     nil)
    ((= n 2)
     t)
    ((evenp n)
     nil)
    (t
     (do ((i 2 (1+ i)))
     ((= i n) t)
       (when (or (= (mod n i) 0))
         (return nil))))))

(defun next-prime (n)
  (do ((i n (1+ i)))
      ((is-prime i) i)))

(defun prime-iterator (start-at)
  (let ((current start-at))
    (lambda ()
      (let ((next-prime (next-prime current)))
         (setf current (1+ next-prime))
         next-prime))))

(defun map-primes/iterator (fn iterator end)
  (do ((i (funcall iterator) (funcall iterator)))
      ((>= i end) nil)
    (funcall fn i)))

(defun map-primes (fn start end)
  (let ((iterator (prime-iterator start)))
    (map-primes/iterator fn iterator end)))

(defmacro do-primes ((var start end) &body body)
  `(map-primes #'(lambda (,var)
                   ,@body)
               ,start ,end))

Я тоже рекомендую вам посмотреть Series. Шаблон генератора также очень часто встречается в программах lisp. Вы также можете посмотреть Alexandria, в частности функцию ALEXANDRIA: COMPOSE, чтобы посмотреть, какой классный материал вы можете сделать с функциональной композицией.

Ответ 5

Я предлагаю избегать DO/DO * и макросов в целом и вместо этого переходить на Series (реализация которого можно найти на series.sourceforge.net).

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