测试某个东西是否为空列表



如果对象是Clojure中的空列表,我应该选择哪种方式进行测试?请注意,我只想测试这个,而不是如果它作为一个序列是空的。如果它是一个"懒惰实体"(LazySeqIterate,…),我不希望它得到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是在假设每个空列表都是同一个对象的情况下进行的。我所做的测试证实了这一假设,但它能保证成立吗?即使是这样,依靠这一事实是否一种好的做法?

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


更新

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

使用"列表"一词有点令人困惑。毕竟什么是清单?如果它是像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)))

也就是说,由于短路和LazySeq没有实现IPersistentList的事实,推荐的测试1也适用于我的情况。鉴于这个问题的表述并不理想,我将接受李简洁的回答,并感谢艾伦·汤普森的时间和我们以赞成票进行的有益的小型讨论。

应该避免选项0,因为它依赖于clojure.lang中的一个类,该类不是包的公共API的一部分:来自clojure.lang:的javadoc

唯一被认为是公共API一部分的类是IFn。所有其他类应该被视为实现细节。

选项1使用公共API中的函数,并避免在非空的情况下迭代整个输入序列

选项2迭代整个输入序列以获得计数,这可能是昂贵的。

选项3似乎没有得到保证,可以通过反射绕过:

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

考虑到这些,我更喜欢选项1。

只需使用选项(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?并没有实现无限懒惰seq。


更新

选择0取决于实现细节(不太可能更改,但为什么要麻烦呢?)。此外,读书更吵。

选择2将爆炸为无限懒惰的seq。

选择3不能保证有效。您可以有多个不包含任何元素的列表。


更新#2

好的,你的re(2)是正确的。我们得到:

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

请注意,并不像您和我所期望的那样是一个懒惰的序列

因此,您依赖于一个(不明显的)细节来防止到达count,这将导致无限懒惰的seq。对我的口味来说太微妙了。我的座右铭:保持尽可能明显

再次选择(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.A.R.囤积

最新更新