测试某物是否为空列表

testing if something is an empty list

在 Clojure 中,我应该使用哪种方法来测试对象是否为空列表?请注意,我只想测试这个,而不是它作为一个序列是否为空。如果它是 "lazy entity" (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水平有点低,依赖"implementation details"。我的第一个版本是 (instance? clojure.lang.PersistentList$EmptyList x),它给出了 IllegalAccessError。为什么呢?这样的测试不应该吗?

测试 1 和 2 级别更高,更通用,因为 list? 检查是否有东西实现 IPersistentList。我想他们的效率也略低。请注意,两个子测试的顺序很重要,因为我们依赖于短路。

测试 3 假设每个空列表都是同一个对象。我所做的测试证实了这个假设,但它能保证成立吗?即使是这样,依靠这个事实是一个好的做法吗?

所有这一切看起来微不足道,但我有点困惑,没有为这样一个简单的任务找到一个完全直接的解决方案(甚至是一个内置函数)。


更新

也许我没有很好地表述问题。回想起来,我意识到我想要测试的是某个东西是否是一个非懒惰的空序列。我的用例最关键的要求是,如果它是一个惰性序列,它不会被实现,即没有强制转换

使用术语 "list" 有点令人困惑。毕竟什么是列表?如果它是像 PersistentList 这样具体的东西,那么它是非惰性的。如果它是像 IPersistentList 这样抽象的东西(这是 list? 测试的并且可能是正确答案),那么就不能完全保证非惰性。恰好Clojure当前的惰性序列类型没有实现这个接口。

所以首先我需要一种方法来测试某些东西是否是惰性序列。我现在能想到的最好的解决方案是使用 IPending 来测试一般的惰性:

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

尽管有一些惰性序列类型(例如像 RangeLongRange 这样的分块序列)没有实现 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 的事实。考虑到这一点以及问题的表述不是最优的,我将接受 Lee 的简洁回答,并感谢 Alan Thompson 抽出宝贵的时间以及我们在投票中进行的有益的小型讨论。

只需使用选项 (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,这将导致无限惰性 seq。对我来说太微妙了。我的座右铭:保持尽可能明显

关于选择 (3),它再次依赖于(当前版本)Clojure 的实现细节。除了 clojure.lang.PersistentList$EmptyList 是一个受包保护的内部 class,我几乎可以让它失败,所以我必须真正努力(颠覆 Java 继承)来制作 [= 的重复实例=66=],这将失败。

不过,我可以接近:

(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

所以,我再次请求让它简单明了。


There are two ways of constructing a software design. One way is to make it so simple that there are obviously no deficiencies. And the other way is to make it so complicated that there are no obvious deficiencies. ---C.A.R. Hoare

选项 0 应该避免,因为它依赖于 clojure.lang 中的 class 而不是包的 public API 的一部分:来自 the javadoc for clojure.lang:

The only class considered part of the public API is IFn. All other classes should be considered implementation details.

选项 1 使用 public API 中的函数并避免迭代整个输入序列(如果它是非空的)

选项 2 迭代整个输入序列以获得可能代价高昂的计数。

选项 3 似乎没有保证,可以通过反射来规避:

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

=> false

鉴于这些,我更喜欢选项 1。