如果对象是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是在假设每个空列表都是同一个对象的情况下进行的。我所做的测试证实了这一假设,但它能保证成立吗?即使是这样,依靠这一事实是否一种好的做法?
所有这些可能看起来微不足道,但对于这样一个简单的任务,我没有找到一个完全简单的解决方案(甚至是一个内置函数),这让我有点困惑。
更新
也许我没有很好地阐述这个问题。回想起来,我意识到我想要测试的是某个东西是否是一个非懒惰的空序列。对于我的用例,最关键的要求是,如果它是一个懒惰序列,它就不会被实现,即没有thunk被强制。
使用"列表"一词有点令人困惑。毕竟什么是清单?如果它是像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)))
也就是说,由于短路和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.囤积