Макрос Лиспа (или функция) для вложенных циклов

Можно ли написать макрос Common Lisp, который берет список измерений и переменных, тело (итерации) и создает код, состоящий из такого количества вложенных циклов, как указано в списке?

То есть что-то вроде:

(nested-loops '(2 5 3) '(i j k) whatever_loop_body)

следует расширить до

(loop for i from 0 below 2 do
  (loop for j from 0 below 5 do
    (loop for k from 0 below 3 do
      whatever_loop_body)))

Продолжение

Как правильно указал Хуайюань, я должен знать параметры, которые нужно передать макросу во время компиляции. Если вам действительно нужна функция, как мне, посмотрите ниже.

Если у вас все в порядке с макросом, используйте рекурсивное решение 6502, это замечательно.


person mmj    schedule 15.04.2012    source источник
comment
Какой диалект Лиспа вы используете? Общий Лисп?   -  person seh    schedule 15.04.2012


Ответы (3)


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

(defmacro nested-loops (dimensions variables &body body)
  (loop for range in (reverse dimensions)
        for index in (reverse variables)
        for x = body then (list y)
        for y = `(loop for ,index from 0 to ,range do ,@x)
        finally (return y)))

Редактировать:

Если размеры не могут быть определены во время компиляции, нам понадобится функция

(defun nested-map (fn dimensions)
  (labels ((gn (args dimensions)
             (if dimensions
               (loop for i from 0 to (car dimensions) do
                 (gn (cons i args) (cdr dimensions)))
               (apply fn (reverse args)))))
    (gn nil dimensions)))

и оборачивать тело в лямбду при вызове.

CL-USER> (nested-map (lambda (&rest indexes) (print indexes)) '(2 3 4))

(0 0 0) 
(0 0 1) 
(0 0 2) 
(0 0 3) 
(0 0 4) 
(0 1 0) 
(0 1 1) 
(0 1 2) 
(0 1 3) 
(0 1 4) 
(0 2 0) 
(0 2 1) 
...

Редактировать (2012-04-16):

Вышеупомянутая версия вложенной карты была написана, чтобы более точно отразить исходную постановку задачи. Как mmj сказал в комментариях, вероятно, более естественно сделать индекс диапазоном от 0 до n-1, и перемещение реверсирования из внутреннего цикла должно повысить эффективность, если мы не настаиваем на порядке итераций по строкам. Кроме того, вероятно, более разумно, чтобы входная функция принимала кортеж вместо отдельных индексов, чтобы быть независимой от ранга. Вот новая версия с заявленными изменениями:

(defun nested-map (fn dimensions)
  (labels ((gn (args dimensions)
             (if dimensions
               (loop for i below (car dimensions) do
                 (gn (cons i args) (cdr dimensions)))
               (funcall fn args))))
    (gn nil (reverse dimensions))))

Потом,

CL-USER> (nested-map #'print '(2 3 4))
person huaiyuan    schedule 15.04.2012
comment
Функционал тоже отличный. Мне нравится твое кодирование. Поскольку я предпочитаю, чтобы индексы начинались слева, мне нужно заменить «(обратные аргументы)» просто «аргументами», а в последней строке «размеры» на «(обратные размеры)». - person mmj; 16.04.2012
comment
Если n является значением измерения, я бы предложил, чтобы цикл (в функции) шел либо от 0 до n-1 (мой выбор), либо от 1 до n. В первом случае вам просто нужно заменить «до» на «ниже» в предложении цикла. - person mmj; 16.04.2012

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

(defmacro nested-loops (max-values vars &rest body)
  (if vars
      `(loop for ,(first vars) from 0 to ,(first max-values) do
          (nested-loops ,(rest max-values) ,(rest vars) ,@body))
      `(progn ,@body)))

(nested-loops (2 3 4) (i j k)
  (print (list i j k)))

В приведенном выше примере, если список переменных пуст, макрос расширяется непосредственно до форм тела, в противном случае сгенерированный код представляет собой (loop...) для первой переменной, содержащей еще один вызов (nested-loops ...) в части выполнения.

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

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

person 6502    schedule 15.04.2012
comment
Это то, к чему я стремился, но не смог. Я изучаю ваш код. Есть ли какая-нибудь секретная книга, из которой вы научились такому акробатическому программированию на Лиспе? - person mmj; 16.04.2012
comment
Кажется, что 1-й индекс пропускает последнюю итерацию, т.е. в вашем примере переменная «i» принимает только значения 0 и 1, пропустив значение 2. Или, как лучшая альтернатива, переменные «j» и «k» должны идти от 0 к их значению меньше 1: так что «подсчет» означал, сколько итераций для каждой переменной. - person mmj; 16.04.2012
comment
Просто предложение стиля; попробуйте использовать базовый случай, когда vars и counts пусты. Тогда вся логика макроуровня будет на первом месте и, в зависимости от базового случая, будет генерировать другой код. Тогда код dotimes будет только в небазовой ветке, body будет только в базовой ветке, и у вас должно быть меньше акробатики с обратными кавычками. В @mmj Let Over Lambda есть хорошая глава о написании рекурсивных макросов. - person Clayton Stanley; 16.04.2012
comment
@mmj: я не привык к loops и не знал, что вы ищете конкретную конструкцию. Также обычно я зацикливаюсь от 0 до (1- n) (поэтому n итерации). Обе проблемы исправлены, и теперь макрос соответствует тексту вашего вопроса (честно говоря, я изначально реализовал его на диалекте Лиспа REPL, где нет возможности цикла). - person 6502; 16.04.2012
comment
@claytontstanley: это была моя первая версия, но мне не нравилась идея расширения до формы progn. Для этого действительно нет причин, поэтому я вернулся к формулировке, которая явно проще для понимания. Спасибо что подметил это. - person 6502; 16.04.2012
comment
Я неоднократно замечал этот шаблон (progn ,@body) в своем коде; Я никогда не находил чистого способа обойти эту идиому; или любая практическая причина рассмотреть возможность не использовать его - person Clayton Stanley; 17.04.2012
comment
@ 6502 - Ограничения итераций были неправильными в исходном вопросе, моя вина: теперь я исправил их для выбора 0/n-1. Что касается создаваемой конструкции, мне не нужен макрос для расширения до операторов «цикла», если вы можете достичь той же цели, используя операторы «делать» или «дотаймс», это прекрасно, может быть, даже лучше. - person mmj; 23.04.2012
comment
@ 6502 - После вашего редактирования код стал кристально чистым и удивительно элегантным. Это рекурсивное решение - это именно то, о чем я думал, прежде чем публиковать вопрос. Я действительно хотел бы принять оба ответа. - person mmj; 23.04.2012

Хм. Вот пример такого макроса в Common Lisp. Обратите внимание, однако, что я не уверен, что это действительно хорошая идея. Но мы все здесь взрослые люди, не так ли?

(defmacro nested-loop (control &body body)
  (let ((variables ())
        (lower-bounds ())
        (upper-bounds ()))
    (loop
       :for ctl :in (reverse control)
       :do (destructuring-bind (variable bound1 &optional (bound2 nil got-bound2)) ctl
             (push variable variables)
             (push (if got-bound2 bound1 0) lower-bounds)
             (push (if got-bound2 bound2 bound1) upper-bounds)))
    (labels ((recurr (vars lowers uppers)
               (if (null vars)
                   `(progn ,@body)
                   `(loop 
                       :for ,(car vars) :upfrom ,(car lowers) :to ,(car uppers)
                       :do ,(recurr (cdr vars) (cdr lowers) (cdr uppers))))))
      (recurr variables lower-bounds upper-bounds))))

Синтаксис немного отличается от вашего предложения.

(nested-loop ((i 0 10) (j 15) (k 15 20)) 
    (format t "~D ~D ~D~%" i j k))

расширяется в

(loop :for i :upfrom 0 :to 10
  :do (loop :for j :upfrom 0 :to 15
            :do (loop :for k :upfrom 15 :to 20
                      :do (progn (format t "~d ~d ~d~%" i j k)))))

Первым аргументом макроса является список списка вида

(variable upper-bound)

(с подразумеваемой нижней границей 0) или

(variable lower-bound upper-bounds)

Приложив немного больше любви, можно было бы даже получить что-то вроде

(nested-loop ((i :upfrom 10 :below 20) (j :downfrom 100 :to 1)) ...)

но тогда зачем беспокоиться, если loop уже имеет все эти функции?

person Dirk    schedule 15.04.2012
comment
Я выбираю другой ответ из соображений краткости, но признаю, что ваш более гибкий. Я впечатлен. - person mmj; 15.04.2012