Проверьте правильный список в Common Lisp - PullRequest
3 голосов
/ 16 февраля 2020

Существует ли в Common Lisp стандартная функция, которая может проверять неправильные списки (то есть круглые и пунктирные списки), не сообщая об ошибке? list-length может проверять циклические списки (для них возвращается nil), но сигнализирует type-error при получении точечного списка.

Схема list? обходит весь список, чтобы убедиться, что он не пунктирный или круговой; Common Lisp's listp только проверяет, что ему дано nil или cons-ячейка.

Вот самое простое, что я мог придумать:

(defun proper-list-p (x)
  (not (null (handler-case (list-length x) (type-error () nil)))))

Поскольку было предложено несколько реализаций и многие неожиданные проблемы были найдены, вот набор тестов для начинающих proper-list-p авторов:

(defun circular (xs)
  (let ((xs (copy-list xs)))
    (setf (cdr (last xs)) xs)
    xs))

(assert (eql t (proper-list-p '())))
(assert (eql t (proper-list-p '(1))))
(assert (eql t (proper-list-p '(1 2))))
(assert (eql t (proper-list-p '(1 2 3))))

(assert (not (proper-list-p 1)))
(assert (not (proper-list-p '(1 . 2))))
(assert (not (proper-list-p '(1 2 . 3))))
(assert (not (proper-list-p '(1 2 3 . 4))))

(assert (not (proper-list-p (circular '(1)))))
(assert (not (proper-list-p (circular '(1 2)))))
(assert (not (proper-list-p (circular '(1 2 3)))))
(assert (not (proper-list-p (list* 1 (circular '(2))))))
(assert (not (proper-list-p (list* 1 2 (circular '(3 4))))))

Ответы [ 4 ]

2 голосов
/ 16 февраля 2020
Кроме того,

, что-то немного менее многословное, чем принятый ответ:

(defun improper-tail (ls)
  (do ((x ls (cdr x))
       (visited nil (cons x visited)))
      ((or (not (consp x)) (member x visited)) x)))

(defun proper-list-p (ls)
  (null (improper-tail ls)))

или просто так:

(defun proper-list-p (ls)
  (do ((x ls (cdr x))
       (visited nil (cons x visited)))
      ((or (not (consp x)) (member x visited)) (null x))))

видно, что он прошел все тестовые утверждения ОП

2 голосов
/ 16 февраля 2020

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

Минимальная (не очень производительная) реализация, которая не основывается на обработке ошибок (Python люди думают, что это разумный способ программирования, я не думаю, хотя это - стилистический c выбор), я думаю,

(defun proper-list-p (l)
  (typecase l
    (null t)
    (cons
     (loop for tail = l then (cdr tail)
           for seen = (list tail) then (push tail seen)
           do (cond ((null tail)
                     (return t))
                    ((not (consp tail))
                     (return nil))
                    ((member tail (rest seen))
                     (return nil)))))))

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

Я уверен, что в библиотеках есть намного лучшие функции, чем эта. В частности Александрия имеет один.


Размышляя над этим вопросом, я также написал эту функцию:

(defun classify-list (l)
  "Classify a possible list, returning four values.

The first value is a symbol which is
- NULL if the list is empty;
- LIST if the list is a proper list;
- CYCLIC-LIST if it contains a cycle;
- IMPROPER-LIST if it does not end with nil;
- NIL if it is not a list.

The second value is the total number of conses in the list (following
CDRs only).  It will be 0 for an empty list or non-list.

The third value is the cons at which the cycle in the list begins, or
NIL if there is no cycle or the list isn't a list.

The fourth value is the number if conses in the cycle, or 0 if there is no cycle.

Note that you can deduce the length of the leading element of the list
by subtracting the total number of conses from the number of conses in
the cycle: you can then use NTHCDR to pull out the cycle."
  ;; This is written as a tail recursion, I know people don't like
  ;; that in CL, but I wrote it for me.
  (typecase l
    (null (values 'null 0 nil 0 0))
    (cons
     (let ((table (make-hash-table)))
       (labels ((walk (tail previous-tail n)
                  (typecase tail
                    (null
                     (values 'list n nil 0))
                    (cons
                     (let ((m (gethash tail table nil)))
                       (if m
                           (values 'cyclic-list n tail (- n m))
                         (progn
                           (setf (gethash tail table) n)
                           (walk (cdr tail) tail (1+ n))))))
                    (t
                     (values 'improper-list n previous-tail 0)))))
         (walk l nil 0))))
    (t (values nil 0 nil 0))))

Это можно использовать, чтобы получить кучу информации о списке: как долго это будет, если это правильно, если нет, если это cycli c, и где цикл. Помните, что в случае циклических c списков это вернет круговую структуру в качестве третьего значения. Я полагаю, что для этого вам нужно использовать проверку событий - черепаха и заяц сообщат вам, если список циклический c, но не там, где начинается цикл.

1 голос
/ 17 февраля 2020

После наших безнадежных попыток с tailp, здесь, sth, который использует точное представление круговых списков :).

С регулярным выражением (для обнаружения кругового подсписка)

(setf *print-circle* t)

(ql:quickload :cl-ppcre)

(defun proper-listp (lst)
  (or (null lst)                                                   ; either a `'()` or:
      (and (consp lst)                                             ; a cons
           (not (cl-ppcre::scan "#\d+=(" (princ-to-string lst))))  ; not circular
           (null (cdr (last lst))))))                              ; not a dotted list

Без регулярного выражения (не может обнаружить круговые подсписки)

(defun proper-listp (lst)
  (or (null lst)                                                   ; either a `'()` or:
      (and (consp lst)                                             ; a cons
           (not (string= "#" (subseq (princ-to-string lst) 0 1)))  ; not circular
           (null (cdr (last lst))))))                              ; not a dotted list
0 голосов
/ 17 февраля 2020

(tailp l (cdr l)) - это t для циклических списков, но nil для некруглых списков.

Кредиты @tfp и @RainerJoswig, которые научили меня этому здесь .

Итак, ваша функция будет:

(defun proper-listp (lst)
  (or (null lst)                           ; either a `'()` or:
      (and (consp lst)                     ; a cons
           (not (tailp lst (cdr lst)))     ; not circular
           (null (cdr (last lst))))))      ; not a dotted list

Кстати Я использую proper-listp по назначению. Правильно будет - по конвекции proper-list-p. Однако это имя уже занято в CLISP реализации SYSTEM::%PROPER-LIST-P, почему определение функции вызывает постоянную ошибку.

Заключение нашего обсуждения в разделе комментариев:

Поведение tailp для циклических списков не определено. Поэтому этот ответ неверен! Спасибо @Lassi за это!

...