Уточнение функции lisp

Я выполнил упражнение 5 из Graham Common Lisp, глава 5, для которого требуется функция, принимающая объект X и вектор V и возвращающая список всех объектов, непосредственно предшествующих X в V.

Это работает так:

> (preceders #\a "abracadabra")
(#\c #\d #r)

Я сделал рекурсивную версию:

(defun preceders  (obj vec &optional (result nil) &key (startt 0))
  (let ((l (length vec)))
    (cond ((null (position obj vec :start startt :end l)) result) 
          ((= (position obj vec :start startt :end l) 0)
           (preceders obj vec result 
                      :startt (1+ (position obj vec :start startt :end l))))  
          ((> (position obj vec :start startt :end l) 0)
           (cons (elt vec (1- (position obj vec :start startt :end l))) 
                 (preceders obj vec result 
                            :startt (1+ (position obj vec
                                                 :start startt
                                                 :end l))))))))

Это работает правильно, но мои учителя дают мне следующую критику:

Это многократно вызывает длину. Не так уж и плохо с векторами, но все же ненужно. Более эффективный и более гибкий (для пользователя) код состоит в том, чтобы определить это как другие функции обработки последовательности. Используйте параметры ключевых слов :start и :end так же, как это делают другие функции последовательности, с теми же начальными значениями по умолчанию. length должен вызываться не более одного раза.

Я консультируюсь с учебником по Common Lisp и google, но, похоже, в этом немного помогает: я не знаю, что он имеет в виду, используя параметры ключевого слова :start и :end , и я понятия не имею, как вызывать length только раз. Я был бы признателен, если бы вы, ребята, могли дать мне некоторое представление о том, как улучшить мой код, чтобы он соответствовал требованиям, опубликованным моим учителем.

ОБНОВИТЬ:

Теперь я придумал следующий код:

(defun preceders (obj vec
                  &optional (result nil)
                  &key (start 0) (end (length vec)) (test #'eql))  
  (let ((pos (position obj vec :start start :end end :test test)))  
    (cond ((null pos) result)
          ((zerop pos) (preceders obj vec result
                                 :start (1+ pos) :end end :test test)) 
          (t (preceders obj vec (cons (elt vec (1- pos)) result)
                       :start (1+ pos) :end end :test test)))))

Я получаю эту критику:

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

Кроме того, для моей итеративной версии функции:

(defun preceders (obj vec) 
  (do ((i 0 (1+ i))
       (r nil (if (and (eql (aref vec i) obj) 
                       (> i 0)) 
                  (cons (aref vec (1- i)) r) 
                  r))) 
      ((eql i (length vec)) (reverse r)))) 

Я получаю критику

Начать DO с лучшей точки и убрать повторяющийся › 0 тест


person Kevin    schedule 30.11.2009    source источник


Ответы (5)


типичный список параметров для такой функции:

(defun preceders (item vector 
                  &key (start 0) (end (length vector))
                       (test #'eql))
    ...
)

Как видите, у него есть параметры START и END.

ТЕСТ — это функция сравнения по умолчанию. Используйте (элемент теста funcall (вектор aref i)). Часто есть еще КЛЮЧЕВОЙ параметр...

LENGTH вызывается повторно для каждого рекурсивного вызова PRECEDERS.

Я бы сделал нерекурсивную версию и переместил два индекса по вектору: один для первого элемента и один для следующего элемента. Всякий раз, когда следующий элемент является EQL для элемента, который вы ищете, затем вставьте первый элемент в список результатов (если он там не является членом).

Для рекурсивной версии я бы написал вторую функцию, которая вызывается PRECEDERS, которая принимает две индексные переменные, начинающиеся с 0 и 1, и использовала ее. Я бы не назвал POSITION. Обычно эта функция является локальной функцией через LABELS внутри PRECEDERS, но чтобы упростить ее написание, вспомогательная функция может быть и снаружи.

(defun preceders (item vector 
                  &key (start 0) (end (length vector))
                       (test #'eql))
   (preceders-aux item vector start end test start (1+ start) nil))


(defun preceders-aux (item vector start end test pos0 pos1 result)
  (if (>= pos1 end)
      result
      ...
  ))

Это помогает?

Вот итеративная версия с использованием LOOP:

(defun preceders (item vector 
                  &key (start 0) (end (length vector))
                       (test #'eql))
  (let ((result nil))
    (loop for i from (1+ start) below end
          when (funcall test item (aref vector i))
          do (pushnew (aref vector (1- i)) result))
    (nreverse result)))
person Rainer Joswig    schedule 30.11.2009
comment
Спасибо, Райнер, действительно полезная информация о синтаксисе, очень трудно найти в Интернете такое точное определение функций обработки последовательности, большое спасибо. Сейчас я работаю над тем, чтобы мой код стал намного проще, и я позволю вам, ребята. знать, за что мой учитель будет критиковать меня. - person Kevin; 01.12.2009

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

(defun preceders (obj seq &key (start 0) (end (length seq)) (test #'eql))          
  (%preceders obj seq nil start end test))

Основная причина иметь отдельную вспомогательную функцию (которую я называю %PRECEDERS, общепринятое соглашение для обозначения того, что функция является «частной») состоит в том, чтобы исключить необязательный аргумент для результата. Использование необязательных аргументов таким образом в целом нормально, но необязательные аргументы и аргументы с ключевыми словами ужасно сочетаются друг с другом, и наличие обоих в одной функции является чрезвычайно эффективным способом создания всех видов трудно отлаживаемых ошибок.

Делать вспомогательную функцию глобальной (используя DEFUN) или локальной (используя LABELS) — дело вкуса. Я предпочитаю делать его глобальным, так как это означает меньше отступов и более простую интерактивную отладку. YMMV.

Возможная реализация вспомогательной функции:

(defun %preceders (obj seq result start end test)
  (let ((pos (position obj seq :start start :end end :test test)))
       ;; Use a local binding for POS, to make it clear that you want the 
       ;; same thing every time, and to cache the result of a potentially 
       ;; expensive operation. 
    (cond ((null  pos) (delete-duplicates (nreverse result) :test test))             
          ((zerop pos) (%preceders obj seq result (1+ pos) end test))
          ;; I like ZEROP better than (= 0 ...). YMMV.
          (t (%preceders obj seq 
                         (cons (elt seq (1- pos)) result)
                         ;; The other little bit of work to make things 
                         ;; tail-recursive.      
         (1+ pos) end test)))))

Кроме того, после всего этого я думаю, что должен указать, что я также согласен с советом Райнера сделать это с помощью явного цикла вместо рекурсии, при условии, что рекурсивное выполнение этого не является частью упражнения.

EDIT: я переключился на более распространенное соглашение "%" для вспомогательной функции. Обычно любое соглашение, которое вы используете, лишь дополняет тот факт, что вы явно экспортируете только те функции, которые составляют ваш общедоступный интерфейс, но некоторые стандартные функции и макросы используют завершающий «*» для обозначения вариантов функциональности.

Я изменил вещи, чтобы удалить повторяющиеся прецеденты, используя стандартную функцию DELETE-DUPLICATES. Это может быть намного (то есть экспоненциально) быстрее, чем многократное использование ADJOIN или PUSHNEW, поскольку оно может использовать хешированное представление множества внутри, по крайней мере, для общих тестовых функций, таких как EQ, EQL и EQUAL.

person Pillsy    schedule 30.11.2009
comment
Pillsy, большое спасибо. Ваш фрагмент кода действительно помогает мне в формировании новой структуры этой функции. Теперь я могу использовать :start :end и :test для построения своей функции. использовать pushnew, чтобы избавиться от повторяющихся элементов, может произойти во время процесса. Однако моему учителю не нравится pushnew, он говорит, что нет необходимости использовать Pushnew или Setf в какой-либо рекурсивной функции, поэтому мне действительно нужно подумать какой-либо другой способ избавиться от повторяющихся элементов. - person Kevin; 01.12.2009
comment
также: внутренние вспомогательные переменные, такие как RESULT, не должны быть частью общедоступного списка параметров, даже в качестве &необязательных переменных. - person Rainer Joswig; 01.12.2009
comment
Именование foo* часто используется для разных вещей, таких как «распространенные» и «нераспространенные» аргументы. - person Rainer Joswig; 01.12.2009
comment
Вместо PUSHNEW, возможно, вы хотите ADJOIN? Google для clhs примыкает. - person huaiyuan; 01.12.2009

Слегка модифицированный вариант версии петли Райнера:

(defun preceders (item vector 
                  &key (start 0) (end (length vector))
                  (test #'eql))
  (delete-duplicates
   (loop
      for index from (1+ start) below end 
      for element = (aref vector index) 
      and previous-element = (aref vector (1- index)) then element
      when (funcall test item element)
      collect previous-element)))

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

person HD.    schedule 02.12.2009

Ответ на ваше первое ОБНОВЛЕНИЕ.

Первый вопрос:

видеть это

(if (foo)
  (bar (+ 1 baz))
  (bar baz))

Это то же самое, что:

(bar (if (foo)
        (+ 1 baz)
        baz))

or:

(let ((newbaz (if (foo)
                 (+ 1 baz)
                 baz)))
  (bar newbaz))

Второй:

Почему бы не начать с I = 1 ?

Смотрите также итеративную версию в моем другом ответе...

person Rainer Joswig    schedule 01.12.2009

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

Если требуется рекурсивная версия, другой подход состоит в том, чтобы продвигать start до тех пор, пока он не встретится с end, собирая результат по пути.

(defun precede (obj vec &key (start 0) (end (length vec)) (test #'eql))
  (if (or (null vec) (< end 2)) nil
    (%precede-recur obj vec start end test '())))

(defun %precede-recur (obj vec start end test result)
  (let ((next (1+ start)))
    (if (= next end) (nreverse result)
      (let ((newresult (if (funcall test obj (aref vec next))
                         (adjoin (aref vec start) result)
                         result)))
        (%precede-recur obj vec next end test newresult)))))

Конечно, это всего лишь еще один способ выразить версию loop.

тестовое задание:

[49]> (precede #\a "abracadabra") 
(#\r #\c #\d)
[50]> (precede #\a "this is a long sentence that contains more characters") 
(#\Space #\h #\t #\r)
[51]> (precede #\s "this is a long sentence that contains more characters")
(#\i #\Space #\n #\r)

Кроме того, мне интересно, Роберт, сказал ли ваш учитель, почему ему не нравится использовать adjoin или pushnew в рекурсивном алгоритме?

person Jorge Gajon    schedule 02.12.2009
comment
Использование POSITION не проблема, если вы переместите :START соответствующим образом, хотя эти элементы вектора не будут затронуты дважды или более. В решении упражнения я бы избегал POSITION, так как это относительно сложная библиотечная функция (с большим количеством аргументов и работой над последовательностями) и здесь она особо не нужна. Однако в «реальном» коде я часто использую POSITION, пытаясь убедиться, что он не выполняется дважды для одних и тех же элементов. - person Rainer Joswig; 02.12.2009
comment
Райнер, теперь я вижу, я не обращал достаточно внимания. Вы правы, использование POSITION не должно вызывать проблем. Спасибо! - person Jorge Gajon; 02.12.2009