Mapcar на месте: деструктивно изменить список списков

У меня есть список списков: (setq xs (list (list 1 2 3) (list 4 5 6) (list 7 8 9))). Я хочу удалить первый элемент из каждого списка, чтобы получить ((2 3) (5 6) (8 9)). Это легко сделать неразрушающим образом: (mapcar 'cdr xs). Но я хочу изменить исходный список. Я попытался:

(mapcar (lambda (x) (setf x (cdr x))) xs)
(mapcar (lambda (x) (pop x)) xs)

Но это не работает. Как изменить каждый список переменных xs на месте, не создавая никаких временных списков, максимально эффективно?


person Mirzhan Irkegulov    schedule 27.12.2014    source источник


Ответы (2)


Используйте 1_:

CL-USER 16 > (let ((s (list (list 1 2 3)
                            (list 4 5 6)
                            (list 7 8 9))))
               (map-into s #'rest s))
((2 3) (5 6) (8 9))
person Rainer Joswig    schedule 27.12.2014

Ответ @Rainer Joswig правильный, используйте map-into. В ссылке приведен пример реализации с использованием макроса loop. Если вы хотите реализовать map-into с нуля или используете Emacs Lisp, вы также можете сделать это с помощью dotimes. В Emacs Lisp dotimes реализован в subr.el и не требует CL пакет. Это map-into с 1 последовательностью для отображения в результирующую последовательность:

(defun map-into (r f xs)
  (dotimes (i (min (length r) (length xs)) r)
    (setf (elt r i)
          (funcall f (elt xs i)))))

Для версии с переменным количеством последовательностей мы должны посыпать наш код apply и mapcar:

(defun map-into (r f &rest xss)
  (dotimes (i (apply 'min (length r) (mapcar 'length xss)) r)
    (setf (elt r i)
          (apply f (mapcar (lambda (s) (elt s i))
                           xss)))))

Однако мы видим, что elt внутри dotimes заставляет наш алгоритм работать за O(n2). Мы можем оптимизировать его для работы в O(n), используя mapl. (спасибо @Joshua Taylor).

(defun map-into (rs f xs)
  (mapl (lambda (r x) (setf (car r) (funcall f (car x)))) rs xs))

(defun map-into (rs f &rest xss)
  (mapl (lambda (r xs)
          (setf (car r)
                (apply f (car xs))))
        rs
        (apply 'mapcar 'list xss))) ;; transpose a list of lists

Причина, по которой setf не работает внутри mapcar, заключается в том, что setf является сложный макрос, который преобразуется в выражение, которое может манипулировать изменяемыми данными. В лямбда-области внутри mapcar он имеет доступ только к переменной, локальной для этой лямбды, а не к последовательности, переданной самому mapcar, так как же он должен знать, куда вернуть измененное значение? Вот почему код mapcar в вопросе возвращает измененный список списков, но не изменяет его на месте. Просто попробуйте (macroexpand '(setf (elt xs 0) (funcall 'cdr (elt xs 0)))) и убедитесь сами.

person Mirzhan Irkegulov    schedule 27.12.2014
comment
Использование elt для доступа к элементам списка, как это, дорого. Это делает функцию за время n‹sup›2‹/sup› вместо n. Было бы лучше отслеживать хвосты списка, чтобы вы всегда получали первый элемент. Для этого можно использовать mapl. эффективно. Например, чтобы удалить первый элемент из каждого из списков, вы можете (mapl (лямбда (хвост) (поп (первый хвост))) список). - person Joshua Taylor; 27.12.2014
comment
См. редактирование. Работают ли новые функции за O(n)? Кроме того, возможно ли упростить отображение переменных аргументов? - person Mirzhan Irkegulov; 30.12.2014
comment
Новые функции — O(n), и это хорошо. Я думаю, вы можете упростить вторую реализацию map-into. Например, см. pastebin.com/W8tLkVBR. - person Joshua Taylor; 31.12.2014