тестирование, если что-то пустой список - PullRequest
0 голосов
/ 06 марта 2020

Каким способом я предпочитаю проверить, является ли объект пустым списком в 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 работает в предположении, что каждый пустой список является одним и тем же объектом. Проведенные мною тесты подтверждают это предположение, но гарантированно ли оно выполнено? Даже если это так, стоит ли полагаться на этот факт?

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


update

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

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

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

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

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

Теперь, полагаясь на короткое замыкание, чтобы предотвратить реализацию на 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. Учитывая то, что формулировка вопроса была неоптимальной, я приму краткий ответ Ли и благодарю Алана Томпсона за его время и за полезную мини-дискуссию, которую мы провели с поднятым голосом.

Ответы [ 2 ]

1 голос
/ 08 марта 2020

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

Единственным классом, который считается частью API publi c, является IFn. Все остальные классы должны рассматриваться как детали реализации.

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

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

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

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

=> false

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

1 голос
/ 07 марта 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), бесконечный ленивый seq didn ' empty?.


Обновление

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

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

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


Update # 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

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


Существует два способа конструирования программного обеспечения. Один из способов - сделать это настолько простым, чтобы не было никаких недостатков. И другой способ - сделать это настолько сложным, чтобы не было явных недостатков. --- C .AR Hoare

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...