Поиск соседей в 2d-списке, умный способ

#common-lisp

#common-lisp

Вопрос:

Давайте предположим, что у меня есть 2d-список (или массив, если хотите), который выглядит следующим образом:

 '( (I I I O)
   (I X I O)
   (I I I O))
  

А теперь давайте предположим, что я хотел бы найти всех соседей X. В этом случае моя функция вернет список из 8 I:s. Как бы мне реализовать эту функцию разумным способом? Я уже реализовал функцию, которая выглядит следующим образом:

 (defun get-neighbours (board x y)
  (let ((output '() ))
    (push (get-if-possible board (1  x) y) output)
    (push (get-if-possible board (1- x) y) output)
    (push (get-if-possible board x (1  y)) output)
    (push (get-if-possible board x (1- y)) output)
    (push (get-if-possible board (1  x) (1  y)) output)
    (push (get-if-possible board (1- x) (1- y)) output)
    (push (get-if-possible board (1  x) (1- y)) output)
    (push (get-if-possible board (1- x) (1  y)) output)
  output))
  

И это так… Уродливый.

Ответ №1:

Подготовили что-то вроде этого:

 (defconstant  neighbour-offsets 
       '((-1 . -1) (0 . -1) (1 . -1)
         (-1 .  0)          (1 .  0)
         (-1 .  1) (0 .  1) (1 .  1)))
  

и тогда ваша функция может быть такой простой, как

 (defun get-neighbours (board x y)
  (mapcar (lambda (offs)
            (let ((dx (car offs))
                  (dy (cdr offs)))
              (get-if-possible board (  x dx) (  y dy))))
           neighbour-offsets ))
  

Комментарии:

1. О, да, это было бы намного красивее, но при этом сохранило бы простоту того способа, которым я это написал. Спасибо!

2. Предупреждение: следите за определением (буквального) списка как константы. Некоторые реализации будут спокойно терпеть это (Lispworks), другие будут постоянно бросать на вас «попытки переопределить константу foo » (sbcl) sbcl.org/manual/Defining-Constants.html

Ответ №2:

Во-первых, вы все еще находитесь в императивной стране.

 (defun get-neighbours (board x y)
  (let ((output '() ))
    (push (get-if-possible board (1  x) y) output)
    (push (get-if-possible board (1- x) y) output)
    (push (get-if-possible board x (1  y)) output)
    (push (get-if-possible board x (1- y)) output)
    (push (get-if-possible board (1  x) (1  y)) output)
    (push (get-if-possible board (1- x) (1- y)) output)
    (push (get-if-possible board (1  x) (1- y)) output)
    (push (get-if-possible board (1- x) (1  y)) output)
  output))
  

Вы делаете: объявление переменной, привязываете ее к нулю, изменение переменной, возвращаете переменную.

Это просто:

 (defun get-neighbours (board x y)
  (list (get-if-possible board (1  x) y)
        (get-if-possible board (1- x) y)
        (get-if-possible board x      (1  y))
        (get-if-possible board x      (1- y))
        (get-if-possible board (1  x) (1  y))
        (get-if-possible board (1- x) (1- y))
        (get-if-possible board (1  x) (1- y))
        (get-if-possible board (1- x) (1  y))))
  

Вы можете «сократить» код с помощью локального макроса:

 (defun get-neighbours (board x y)
  (macrolet ((all-possible (amp;rest x-y-combinations)
               `(list
                 ,@(loop for (a b) on x-y-combinations by #'cddr
                         collect `(get-if-posssible board ,a ,b)))))
    (all-possible (1  x) y
                  (1- x) y
                  x      (1  y)
                  x      (1- y)
                  (1  x) (1  y)
                  (1- x) (1- y)
                  (1  x) (1- y)
                  (1- x) (1  y))))
  

Теперь можно немного абстрагироваться от этого:

 (defmacro invoke-fn-on (fn amp;rest x-y-combinations)
  `(funcall (function ,fn)
            ,@(loop for (a b) on x-y-combinations by #'cddr
                    collect `(get-if-posssible board ,a ,b))))

(defun get-neighbours (board x y)
  (invoke-fn-on list
                (1  x) y
                (1- x) y
                x      (1  y)
                x      (1- y)
                (1  x) (1  y)
                (1- x) (1- y)
                (1  x) (1- y)
                (1- x) (1  y)))
  

О ЦИКЛЕ:

 > (loop for (a b) on '(1 2 3 4 5 6) by #'cddr collect (list a b))
((1 2) (3 4) (5 6))
  

ON перемещает шаблон (a b) по списку (1 2 3 4 5 6). Это дало бы (1 2), (2 3), (3 4), (4 5) и (5 6). С каждой итерацией список перемещается на единицу вперед с помощью CDR для получения оставшегося списка. BY now говорит, что мы перемещаемся по двум элементам, по CDDR, а не по одному элементу, как в CDR. Итак, мы получаем три итерации и пары (1 2), (3 4) и (5 6).

Альтернативой было бы немного упростить ЦИКЛ, введя другую структуру списка для пар координат:

 (defmacro invoke-fn-on (fn x-y-combinations)
  `(funcall (function ,fn)
            ,@(loop for (a b) in x-y-combinations
                    collect `(get-if-posssible board ,a ,b))))

(defun get-neighbours (board x y)
  (invoke-fn-on list
                '(((1  x) y     )
                  ((1- x) y     )
                  (x      (1  y))
                  (x      (1- y))
                  ((1  x) (1  y))
                  ((1- x) (1- y))
                  ((1  x) (1- y))
                  ((1- x) (1  y)))))
  

Комментарии:

1. Хм, ладно, значение ключевых слов «by» и «on» в макросе цикла мне все еще неизвестно. Я по-прежнему предпочитаю ответ Ангуса как наиболее читаемый, но я все равно очень ценю вашу помощь!

2. Я не знал, что вы можете делать такие сложные и полезные вещи с помощью цикла. Голосуем «за»!