【问题标题】:testing if something is an empty list测试某事是否为空列表
【发布时间】:2020-03-06 20:54:16
【问题描述】:

在 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)

Test 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)))

话虽如此,推荐的测试 1 也适用于我的情况,这要归功于短路和 LazySeq 没有实现 IPersistentList 的事实。鉴于这一点以及问题的表述不是最理想的,我会接受 Lee 的简洁回答,并感谢 Alan Thompson 抽出时间和我们进行了有益的小型讨论,并投了赞成票。

【问题讨论】:

    标签: clojure is-empty empty-list


    【解决方案1】:

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

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

    选项 1 使用公共 API 中的函数,如果输入序列非空,则避免迭代整个输入序列

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

    选项 3 似乎无法保证,可以通过反射规避:

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

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

    【讨论】:

    • 请注意,在选项2中,由于短路,您提到的问题只有在被测对象是IPersistentList而不是Counted时才会出现,因为对于Counted count 需要固定的时间。但是当前实现IPersistentList(即PersistentListPersistentQueue)的所有类也实现Counted。无论如何,我仍然更喜欢选项 1 的简单性...
    【解决方案2】:

    只需使用选项(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
    

    所以,我再次请求保持简单明了。


    有两种构建软件设计的方法。一种方法是 让它变得如此简单,以至于显然没有任何缺陷。和 另一种方法是让它变得如此复杂以至于没有明显的 不足之处。 - -车。霍尔

    【讨论】:

    • 谢谢。你如何证明这个选择是合理的?您对其他“子问题”也有任何答案吗?
    • (我怀疑其正确性的唯一选项是数字 3。其余的我知道它们有效。)
    • 选择 2 不会因为短路而崩溃(list? 将因LazySeq 而失败)。你能提供一个选择3不起作用的例子吗?或者可能是文档/实施的链接?在我制作空列表的各个方面,它们都是identical?。你也能解释一下为什么instance? 在选择 0 中不起作用吗?
    • 但与我们在选择 1 中所依赖的细节完全相同,以防止 empty? 实现(不完整,只是第一个 Cons)。非常感谢您为选择 3 所做的工作,明天我会带着清晰的思路再次阅读它,但它看起来很有说服力......
    • 至于选项 0:“读起来很吵”的论点不是很强,因为将测试包装在 empty-list? 函数中意味着我们只写一次。在某些情况下,它的具体性难道不是更可取吗?例如。如果有人创建了一个新的惰性序列类型,它也实现了IPersistentList,那么选择 1 和 2 将实现这样的序列。
    猜你喜欢
    • 1970-01-01
    • 2015-12-04
    • 1970-01-01
    • 2023-03-18
    • 1970-01-01
    • 1970-01-01
    • 2012-10-09
    • 2011-02-27
    • 2018-08-10
    相关资源
    最近更新 更多