Выражение лота с небольшим количеством операторов - PullRequest
2 голосов
/ 01 июня 2019

В своей книге Об Лиспе Пол Грэм подчеркивает, что Лисп - это «расширяемый язык». Он говорит, что это означает создание все более высоких языковых интерфейсов в направлении языка, на котором приложение может быть полезно обсуждено или проанализировано. Это приводит к ортогональному языку ... на котором вы можете выразить много, комбинируя небольшое количество операторов множеством разных способов. В качестве эксперимента я хотел бы попытаться расширить одну из более полезных функций последовательности, а именно remove.

Если отложить на время расширения, включающие непоследовательные типы данных (например, удаление элементов из массива, хэш-таблицы или списка свойств), еще есть место для расширения выбора ключевых слов. Например, отсутствует встроенное условие удаления элементов из последовательности на основе их индекса. Вдобавок к этому, программист может захотеть удалить элементы, значение которых совпадает с их индексом, с помощью теста, например, (lambda (elt idx) (= elt idx)) Подход без расширения будет состоять в том, чтобы просто свернуть вашу собственную итеративную функцию (еще одну из ста других трудно запоминаемых утилит), но она кажется более краткой, многократно используемой и эффективной, чтобы воспользоваться преимуществами встроенных функций и расширить их.

Непосредственная проблема заключается в том, что remove применяется только в том случае, если имеется заданный элемент поиска, а для remove-if требуется предикат, который принимает в качестве аргумента только один элемент (не элемент вместе с его индексом). Подход, который я хотел бы изучить, пытается объединить различные опции в одну функцию remove-sequence, где последовательность является единственным обязательным аргументом, а все остальное является ключевым словом, адаптированным к конкретному виду необходимого удаления. Таким образом, элемент поиска указывается в ключевом слове: item, а логический: test с одним или двумя аргументами может включать как элемент, так и индекс, если необходимо. В этом последнем случае простой вызов может выглядеть как (remove-sequence '(3 1 2 4) :test (lambda (elt idx) (= x i))), удаляя третий элемент.

Я начал с функции, которая, кажется, работает на следующих примерах в SBCL:

(require :sb-introspect)

(defun remove-sequence (sequence &key item (test #'eql) from-end (start 0)
                         (end (length sequence)) (count (length sequence)) (key #'identity))
  (cond (item
           (remove item sequence :test test :from-end from-end
                                 :start start :end end :count count :key key))
        ((= (length (sb-introspect:function-lambda-list test)) 1)
           (remove-if test sequence :from-end from-end
                                    :start start :end end :count count :key key))
        ((= (length (sb-introspect:function-lambda-list test)) 2)
           (let* ((delta (if from-end -1 +1))
                  (initial-index (if from-end (length sequence) -1))
                  (closure (let ((index initial-index))
                             (lambda (element)
                               (setf index (+ index delta))
                               ;(print (list element index))
                               (funcall test element index)))))
             (remove-if closure sequence :from-end from-end
                                         :start start :end end :count count :key key)))
        (t (error "in remove-sequence macro"))))

(remove-sequence '(1 2 4 1 3 4 5) :test #'> :item 3) =>  (4 3 4 5)
(remove-sequence '(1 2 3 4 5 6) :test #'evenp :count 2 :from-end t) =>  (1 2 3 5)
(remove-sequence '(3 1 2 4) :test #'(lambda (elt idx) (= elt idx))) =>  (3 1 4)

Однако у меня возникли проблемы с преобразованием его в макрос, который пока выглядит следующим образом. (Он генерирует ошибку во время раскрытия макроса.)

(defmacro remove-sequence (sequence &key item test from-end start end count key)
  (let ((tst (when test `(:test ,test)))
        (frm-nd (when from-end `(:from-end ,from-end)))
        (strt (when start `(:start ,start)))
        (nd (when end `(:end ,end)))
        (cnt (when count `(:count ,count)))
        (ky (when key `(:key ,key)))
        (test-fn (if test test #'eql)))
    (cond (`,item
             `(remove ,item ,sequence ,@tst ,@frm-nd ,@strt ,@nd ,@cnt ,@ky))
          ((= (length (sb-introspect:function-lambda-list test-fn)) 1)
             `(remove-if ,test-fn ,sequence ,@frm-nd ,@strt ,@nd ,@cnt ,@ky))
          ((= (length (sb-introspect:function-lambda-list test-fn)) 2)
             (let* ((delta (if `,from-end -1 +1))
                    (initial-index (if `,from-end (length `,sequence) -1))
                    (closure (let ((index initial-index))
                               (lambda (element)
                                 (setf index (+ index delta))
                                 ;(print (list element index))
                                 (funcall test-fn element index)))))
                `(remove-if ,closure ,sequence ,@frm-nd ,@strt ,@nd ,@cnt ,@ky)))
          (t (error "in remove-sequence macro")))))

Можно ли это исправить? Или есть лучший способ написать это? И, в целом, есть ли обратная сторона в прикреплении, может быть, дюжины или около того ключевых слов? Например, я бы по крайней мере хотел бы добавить логические ключевые слова для: duplicates и: destructive, а другие ключевые слова, вероятно, были бы актуальны для непоследовательных аргументов. Спасибо за любые опытные идеи.

Ответы [ 2 ]

1 голос
/ 03 июня 2019

Вот ваша подпись функции:

(sequence &key 
  item 
  (test #'eql) 
  from-end 
  (start 0)
  (end (length sequence)) 
  (count (length sequence))
  (key #'identity))

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

В стандартной функции REMOVE значением по умолчанию для :END является ноль, что имеет здесь особое значение (конец последовательности) без необходимости фактического вычисления индекса. Функция, которая обрабатывает список, может использовать эту информацию без необходимости обходить весь список; например, вот примерно то, что должно произойти, когда число равно 1:

(defun remove-first (item list)
  (if (endp list)
      nil
      (if (equalp (first list) item)
          (rest list)
          (cons (first list) 
                (remove-first item (rest list))))))

На практике вы можете ожидать, что код не будет полагаться на устранение хвостовой рекурсии:

(defun remove-first (item list)
  (loop
     with stack = nil
     for (head . tail) on list
     do (if (equalp head item)
            (return
              (dolist (e stack tail)
                (push e tail)))
            (push head stack))))

Вы даже можете использовать бесконечные списки:

USER> (setf *print-circle* t)
T

USER> (remove-first 3 '#1=(1 2 3 4 5 6 . #1#))
(1 2 . #1=(4 5 6 1 2 3 . #1#))

Итак, в заключение можно сказать, что в Common Lisp очень приятно то, что стандартные функции / абстракции более высокого уровня имеют предсказуемое, не удивительное использование ресурсов. Даже если он не указан как таковой, я ожидаю, что map в не игрушечных реализациях не вызовет переполнения стека в больших списках из-за рекурсивных вызовов и т. Д. Это хорошо, когда библиотеки экспортируют функции и / или макросы, которые следуют тому же самому подход. Это может быть способом улучшить существующий код.

0 голосов
/ 02 июня 2019

Как заметил Райнер Йосвиг выше, аргумент macro: test - это список, понимаемый как обозначение функции, а не как объект функции. Преобразование его в функцию перед передачей в sb-introspect: function-lambda-list должно исправить ошибку. Кто-то с большим опытом может прокомментировать, охватывают ли symbol-function и coerce все возможные случаи:

(defmacro remove-sequence (sequence &key item (test '(function eql)) from-end start end count (key '(function identity)))
  (let ((tst (when test `(:test ,test)))
        (frm-nd (when from-end `(:from-end ,from-end)))
        (strt (when start `(:start ,start)))
        (nd (when end `(:end ,end)))
        (cnt (when count `(:count ,count)))
        (ky (when key `(:key ,key)))
        (test-fn (cond ((symbolp (second test)) (symbol-function (second test)))
                       ((eq (first test) 'lambda) (coerce test 'function))
                       ((eq (first test) 'function) (coerce (second test) 'function))
                       (t (error "Malformed :test function ~A" test))))
        (key-fn  (cond ((symbolp (second key)) (symbol-function (second key)))
                       ((eq (first key) 'lambda) (coerce key'function))
                       ((eq (first key) 'function) (coerce (second key) 'function))
                       (t (error "Malformed :key function ~A" key)))))
   (cond (`,item
            `(remove ,item ,sequence ,@tst ,@frm-nd ,@strt ,@nd ,@cnt ,@ky))
         ((= 1 (length (sb-introspect:function-lambda-list test-fn)))
            `(remove-if ,test ,sequence ,@frm-nd ,@strt ,@nd ,@cnt ,@ky))
         (t (let* ((delta (if `,from-end -1 +1))
                   (initial-index (if `,from-end (length `,sequence) -1))
                   (closure (let ((index initial-index))
                              (lambda (element)
                                (setf index (+ index delta))
                                ;(print (list element index))
                                (funcall test-fn (funcall key-fn element) index)))))
              `(remove-if ,closure ,sequence ,@frm-nd ,@strt ,@nd ,@cnt ,@ky))))))

Обратите внимание, что по-прежнему существует проблема захвата переменных, которую необходимо решить при следующем наброске.

Другая проблема связана с длиной списка аргументов, возвращаемого sb-introspect: function-lambda-list. Если функция: test имеет только один аргумент, тогда remove-if - правильное расширение. Если он имеет два или более аргумента, то расширение может быть либо remove (если есть также ключевое слово: item), либо remove-if с закрытием (в противном случае). Нет необходимости проверять ровно два аргумента. На самом деле многие приемлемые: у тестовых функций лямбда-список длиннее 2 (например, # '>).

* (remove-sequence '(1 2 4 1 3 4 5) :test #'> :item 3)
 (4 3 4 5)
* (remove-sequence '(1 2 3 4 5 6) :test #'evenp :count 2 :from-end t)
 (1 2 3 5)
* (remove-sequence '(3 1 2 4) :test (lambda (elt idx) (= elt idx)))
 (3 4)
* (defun element=index (elt idx)
     (= elt idx)) ELEMENT=INDEX
* (remove-sequence '(3 1 2 4) :test 'element=index)
 (3 4)
*
...