тестирование, если что-то является пустым списком

Какой способ я предпочитаю проверять, является ли объект пустым списком в Clojure? Обратите внимание, что я хочу проверить именно это, а не то, пуста ли она как последовательность. Если это "ленивый объект" (LazySeq, Iterate,...), я не хочу, чтобы он получал realized?.

Ниже я привожу несколько возможных тестов для x.

;0
(= clojure.lang.PersistentList$EmptyList (class x))

;1
(and (list? x) (empty? x))

;2
(and (list? x) (zero? (count x)))

;3
(identical? () x)

Тест 0 немного низкоуровневый и зависит от «деталей реализации». Моя первая версия была (instance? clojure.lang.PersistentList$EmptyList x), что дает IllegalAccessError. Почему это так? Разве такой тест не возможен?

Тесты 1 и 2 более высокого уровня и более общие, так как list? проверяет, реализует ли что-то IPersistentList. Я думаю, что они также немного менее эффективны. Обратите внимание, что порядок двух подтестов важен, поскольку мы полагаемся на короткое замыкание.

Тест 3 работает в предположении, что каждый пустой список является одним и тем же объектом. Проведенные мной тесты подтверждают это предположение, но гарантируется ли оно? Даже если это так, стоит ли полагаться на этот факт?

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


Обновить

Возможно, я не очень хорошо сформулировал вопрос. Оглядываясь назад, я понял, что хотел проверить, является ли что-то неленивой пустой последовательностью. Наиболее важным требованием для моего варианта использования является то, что если это ленивая последовательность, она не реализуется, то есть преобразователь не выполняется принудительно.

Использование термина «список» немного сбивает с толку. Ведь что такое список? Если это что-то конкретное, например PersistentList, то оно неленивое. Если это что-то абстрактное вроде IPersistentList (это то, что проверяет list? и, вероятно, правильный ответ), то отсутствие лени точно не гарантируется. Так уж получилось, что текущие ленивые типы последовательностей Clojure не реализуют этот интерфейс.

Итак, прежде всего мне нужен способ проверить, является ли что-то ленивой последовательностью. Лучшее решение, которое я могу придумать прямо сейчас, — это использовать IPending для проверки лени в целом:

(def lazy? (partial instance? clojure.lang.IPending))

Хотя существуют некоторые типы ленивых последовательностей (например, фрагментированные последовательности, такие как Range и LongRange), которые не реализуют IPending, кажется разумным ожидать, что ленивые последовательности реализуют его в целом. LazySeq делает это, и это действительно важно в моем конкретном случае использования.

Теперь, полагаясь на короткое замыкание, чтобы предотвратить реализацию empty? (и не дать ему неприемлемый аргумент), мы имеем:

(defn empty-eager-seq? [x] (and (not (lazy? x)) (seq? x) (empty? x)))

Или, если мы знаем, что имеем дело с последовательностями, как в моем случае, мы можем использовать менее строгие ограничения:

(defn empty-eager? [x] (and (not (lazy? x)) (empty? x)))

Конечно, мы можем написать безопасные тесты для более общих типов, таких как:

(defn empty-eager-coll? [x] (and (not (lazy? x)) (coll? x) (empty? x)))
(defn empty-eager-seqable? [x] (and (not (lazy? x)) (seqable? x) (empty? x)))

При этом рекомендуемый тест 1 также работает для моего случая благодаря сокращению и тому факту, что LazySeq не реализует IPersistentList. Учитывая это и то, что формулировка вопроса была неоптимальной, я приму краткий ответ Ли и поблагодарю Алана Томпсона за его время и за полезную мини-дискуссию, которую мы провели с голосованием.


person peter pun    schedule 06.03.2020    source источник


Ответы (2)


Варианта 0 следует избегать, так как он основан на классе в clojure.lang, который не является частью общедоступного API для пакета: From документ Java для clojure.lang:

Единственным классом, который считается частью общедоступного API, является IFn. Все остальные классы следует рассматривать как детали реализации.

Вариант 1 использует функции из общедоступного API и избегает повторения всей входной последовательности, если она не пуста.

Вариант 2 итерирует всю входную последовательность, чтобы получить количество, которое потенциально дорого.

Вариант 3 не гарантируется, и его можно обойти с помощью отражения:

(identical? '() (.newInstance (first (.getDeclaredConstructors (class '()))) (into-array [{}])))

=> false

Учитывая это, я бы предпочел вариант 1.

person Lee    schedule 07.03.2020
comment
Обратите внимание, что в варианте 2 из-за короткого замыкания проблема, о которой вы упомянули, появится только в том случае, если тестируемый объект является IPersistentList, а не Counted, потому что для Counted count требуется постоянное время. Но все классы, которые в настоящее время реализуют IPersistentList (то есть PersistentList и PersistentQueue), также реализуют Counted. Во всяком случае, я все еще предпочитаю простоту варианта 1... - person peter pun; 11.03.2020

Просто используйте выбор (1):

(ns tst.demo.core
  (:use tupelo.core tupelo.test) )

(defn empty-list? [arg] (and (list? arg)
                          (not (seq arg))))
(dotest
  (isnt (empty-list? (range)))
  (isnt (empty-list? [1 2 3]))
  (isnt (empty-list? (list 1 2 3)))

  (is (empty-list? (list)))
  (isnt (empty-list? []))
  (isnt (empty-list? {}))
  (isnt (empty-list? #{})))

с результатом:

-------------------------------
   Clojure 1.10.1    Java 13
-------------------------------

Testing tst.demo.core

Ran 2 tests containing 7 assertions.
0 failures, 0 errors.

Как видно из первого теста с (range), бесконечная ленивая последовательность не была реализована с помощью empty?.


Обновить

Выбор 0 зависит от деталей реализации (вряд ли изменится, но зачем беспокоиться?). Кроме того, читать шумнее.

Выбор 2 взорвется для бесконечных ленивых последовательностей.

Вариант 3 не гарантирует работу. У вас может быть более одного списка с нулевыми элементами.


Обновление №2

Хорошо, вы правы в отношении (2). Мы получили:

(type (range)) => clojure.lang.Iterate

Обратите внимание, что это не Lazy-Seq, как мы с вами ожидали.

Таким образом, вы полагаетесь на (неочевидную) деталь, чтобы предотвратить переход к count, который взорвется для бесконечной ленивой последовательности. Слишком тонко на мой вкус. Мой девиз: делайте это как можно более очевидным

Что касается выбора (3), опять же он зависит от деталей реализации (текущей версии) Clojure. Я мог бы почти заставить его потерпеть неудачу, за исключением того, что clojure.lang.PersistentList$EmptyList является внутренним классом, защищенным пакетом, поэтому мне пришлось бы очень сильно постараться (разрушить наследование Java), чтобы создать дубликат экземпляра класса, который затем потерпит неудачу.

Тем не менее, я могу подойти ближе:

(defn el3? [arg] (identical? () arg))

(dotest
  (spyx (type (range)))
  (isnt (el3? (range)))
  (isnt (el3? [1 3 3]))
  (isnt (el3? (list 1 3 3)))

  (is (el3? (list)))
  (isnt (el3? []))
  (isnt (el3? {}))
  (isnt (el3? #{}))

  (is (el3? ()))
  (is (el3? '()))
  (is (el3? (list)))
  (is (el3? (spyxx (rest [1]))))

  (let [jull (LinkedList.)]
    (spyx jull)
    (spyx (type jull))
    (spyx (el3? jull))) ; ***** contrived, but it fails *****

с результатом

jull => ()
(type jull) => java.util.LinkedList
(el3? jull) => false

Итак, я снова прошу сделать это очевидным и простым.


Существует два способа построения дизайна программного обеспечения. Один из способов — сделать его настолько простым, чтобы в нем явно не было недостатков. И другой способ — сделать его настолько сложным, чтобы не было явных недостатков. ---МАШИНА. Хор

person Alan Thompson    schedule 06.03.2020
comment
Спасибо. Как вы обосновываете этот выбор? У вас есть ответы и на другие подвопросы? - person peter pun; 07.03.2020
comment
(Единственный вариант, в правильности которого я сомневаюсь, это номер 3. В остальном я знаю, что они работают.) - person peter pun; 07.03.2020
comment
Вариант 2 не взорвется из-за короткого замыкания (list? не сработает для LazySeq). Можете ли вы привести пример варианта 3, который не работает? Или, может быть, ссылку на документацию / реализацию? Всячески я производил пустые списки, все они были identical?. Также можете объяснить, почему instance? не работает при выборе 0? - person peter pun; 07.03.2020
comment
НО точно так же подробно мы полагаемся в выборе 1, чтобы предотвратить реализацию (не полную, только первую Cons) empty?. Большое спасибо за работу, которую вы проделали для выбора 3, я прочитаю это снова завтра с ясным умом, но это выглядит убедительно... - person peter pun; 07.03.2020
comment
Что касается выбора 0: аргумент шумного чтения не очень силен, поскольку обертывание теста в функцию empty-list? означает, что мы пишем его только один раз. Также не может ли его конкретность быть более желательной в некоторых случаях? Например. если кто-то создаст новый тип ленивой последовательности, который также реализует IPersistentList, варианты 1 и 2 реализуют такую ​​последовательность. - person peter pun; 07.03.2020
comment
Возможно, идеальным решением был бы высокоуровневый способ проверки того, является ли что-то неленивым списком (таким образом определяя эту концепцию), которого, похоже, не существует. Можем ли мы проверить, реализует ли он IPending для этой цели? - person peter pun; 07.03.2020
comment
(Я только что придумал собственный девиз: добиться простоты может быть сложно. Очевидность может быть обманчивой.) - person peter pun; 07.03.2020