Что не так со следующим макросом Common Lisp, использующим gensym? - PullRequest
10 голосов
/ 16 февраля 2009

Изучение Common Lisp (с использованием GNU CLISP 2.43) .. поэтому может быть ошибкой нуба. Примером является «печать простых чисел между 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)) ;

Ответы [ 5 ]

5 голосов
/ 16 февраля 2009

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

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

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

4 голосов
/ 16 февраля 2009

Вам на самом деле не нужно gensym здесь, чтобы избежать захвата переменных, потому что вы не вводите никаких переменных, которые были бы "локальными для макроса". Когда вы расширите макрокоманду 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 голосов
/ 16 февраля 2009

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

1 голос
/ 17 февраля 2009

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

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

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

(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: COMPOSE, чтобы увидеть, какие классные вещи вы можете сделать с помощью функциональной композиции.

0 голосов
/ 16 февраля 2009

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

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

...