Один из способов проверить ваш код Common Lisp - попросить вашего переводчика TRACE
функции:
(trace removeDuplicates my_member rember)
Чтобы избежать слишком большого количества трассировок, используйте небольшие примеры.
Сначала попробуем с пустым списком; это пример из REPL («read eval print l oop»), протестированного с SBCL, в то время как в пакете «SO» (StackOverflow); трассировка напечатана с небольшим отступом, пронумерована в соответствии с глубиной рекурсии. Здесь вызов не является рекурсивным и сразу завершается:
SO> (removeduplicates nil)
0: (SO::REMOVEDUPLICATES NIL)
0: REMOVEDUPLICATES returned NIL
NIL
Это работает, давайте попробуем пример с одноэлементным списком, в котором явно нет дубликатов:
SO> (removeduplicates '(1))
0: (SO::REMOVEDUPLICATES (1))
1: (SO::MY_MEMBER 1 NIL)
1: MY_MEMBER returned NIL
1: (SO::REMOVEDUPLICATES NIL)
1: REMOVEDUPLICATES returned NIL
0: REMOVEDUPLICATES returned NIL
NIL
removeDuplicate
вызывает my_member
, что правильно возвращает nil
, за которым следует рекурсивный вызов removeDuplicates
с nil
, который правильно возвращает nil
. Однако есть проблема, потому что тогда внешний вызов тоже возвращает nil
, что неверно .
Глядя на трассу, мы должны оглянуться на код, чтобы найти место где вызывается my_member
, за которым следует рекурсивный вызов removeDuplicates
. Существует только одно место, где my_member
называется, в качестве теста для второго предложения в cond
; Поскольку для этого теста результат равен nil
, пробуется следующее предложение, в этом случае значение по умолчанию:
(cond
...
;; this is the call to my_member (= nil)
((my_member (car L) (cdr L)) ...)
;; this is the recursive call
(t (removeDuplicates (cdr L))))
Значение cond
- это значение, данное последним (removeDuplicates (cdr L))
, который просто не сохраняет существующие элементы перед L
. Если бы вы изменяли последовательность, вы могли бы просто выделить подпоследовательность и проигнорировать предыдущие элементы: в этом случае вызывающая сторона все равно будет содержать ссылку на исходную последовательность, что приведет к удалению ее элемента из-за побочного эффекта ваших функций. Но здесь вы придерживаетесь строго неизменного подхода, и вам необходимо восстановить список в качестве возвращаемого значения.
Другими словами, removeDuplicates
выражается как: return a новый список, который содержит те же элементы, что и исходный список, но без дубликатов .
Поэтому необходимо добавить (car L)
перед (removeDuplicates (cdr L))
.
(defun removeDuplicates (L)
(cond
((null L) '())
((my_member (car L) (cdr L)) (rember (car L) L) (removeDuplicates (cdr L)))
(T (cons (car L)
(removeDuplicates (rest L))))))
Давайте test:
SO> (removeduplicates '())
0: (SO::REMOVEDUPLICATES NIL)
0: REMOVEDUPLICATES returned NIL
NIL
SO> (removeduplicates '(1))
0: (SO::REMOVEDUPLICATES (1))
1: (SO::MY_MEMBER 1 NIL)
1: MY_MEMBER returned NIL
1: (SO::REMOVEDUPLICATES NIL)
1: REMOVEDUPLICATES returned NIL
0: REMOVEDUPLICATES returned (1)
(1)
Вы можете проверить с более длинным списком (без дубликатов), результат верный, но трасса длиннее.
Теперь давайте добавим дубликаты:
SO> (removeduplicates '(1 2 2 1))
0: (SO::REMOVEDUPLICATES (1 2 2 1))
1: (SO::MY_MEMBER 1 (2 2 1))
2: (SO::MY_MEMBER 1 (2 1))
3: (SO::MY_MEMBER 1 (1))
3: MY_MEMBER returned T
2: MY_MEMBER returned T
1: MY_MEMBER returned T
1: (SO::REMBER 1 (1 2 2 1))
1: REMBER returned (2 2 1)
1: (SO::REMOVEDUPLICATES (2 2 1))
2: (SO::MY_MEMBER 2 (2 1))
2: MY_MEMBER returned T
2: (SO::REMBER 2 (2 2 1))
2: REMBER returned (2 1)
2: (SO::REMOVEDUPLICATES (2 1))
3: (SO::MY_MEMBER 2 (1))
4: (SO::MY_MEMBER 2 NIL)
4: MY_MEMBER returned NIL
3: MY_MEMBER returned NIL
3: (SO::REMOVEDUPLICATES (1))
4: (SO::MY_MEMBER 1 NIL)
4: MY_MEMBER returned NIL
4: (SO::REMOVEDUPLICATES NIL)
4: REMOVEDUPLICATES returned NIL
3: REMOVEDUPLICATES returned (1)
2: REMOVEDUPLICATES returned (2 1)
1: REMOVEDUPLICATES returned (2 1)
0: REMOVEDUPLICATES returned (2 1)
(2 1)
Результат правильный (порядок значения не имеет).
Пока наши тесты хороши.
Возможно, вы не определили другую проблему в этой функции, а именно, что все звонки на rember
бесполезны, и, честно говоря, это не всегда легко обнаружить с помощью трассировки. Но, глядя на код, должно быть ясно, что если вы пишете код с небольшими побочными эффектами, следующий пункт бесполезно вызывает (rember ...)
:
((my_member (car L) (cdr L)) (rember (car L) L) (removeDuplicates (cdr L)))
Предложение cond имеет синтаксис (TEST . BODY)
, где BODY
- это последовательность выражений, которая оценивается как PROGN
: значение PROGN
- это значение его последнего предложения, все промежуточные предложения используются только для их побочных эффектов. Например:
(progn
(print "I am here")
(* 10 3))
Здесь выше, вызов PRINT
возвращает значение, но оно отбрасывается: значение включающего PROGN
равно 30.
В вашем коде rember
не имеет побочных эффектов, и его возвращаемое значение отбрасывается. Просто удалите его:
(defun removeDuplicates (L)
(cond
((null L) '())
((my_member (car L) (cdr L))
(removeDuplicates (cdr L)))
(T (cons (first L)
(removeDuplicates (rest L))))))
Я бы написал такой же код, как лично:
(defun remove-duplicate-elements (list)
(when list
(let ((head (first list))
(tail (remove-duplicate-elements (rest list))))
(if (member head tail) tail (cons head tail)))))