如何规范lazy-seq生成函数?

时间:2017-02-04 23:40:41

标签: clojure clojure.spec

我希望在生成器函数的前后条件中使用spec。我想要做的简化示例如下所述:

(defn positive-numbers
  ([]
   {:post [(s/valid? (s/+ int?) %)]}
   (positive-numbers 1))
  ([n]
   {:post [(s/valid? (s/+ int?) %)]}
   (lazy-seq (cons n (positive-numbers (inc n))))))

(->> (positive-numbers) (take 5))

然而,定义像这样的生成器函数似乎会导致堆栈溢出,原因是spec会急切地尝试评估整个事情,或者类似的东西....

是否有另一种方法可以使用spec来描述生成器函数的:post结果,如上所述(不会导致堆栈溢出)?

1 个答案:

答案 0 :(得分:2)

理论上正确的答案是,一般来说,你无法检查一个懒惰的序列是否与一个规范相匹配而没有意识到它。

对于(s/+ int?)的具体示例,给定一个惰性序列,如何通过观察序列是否所有元素都是整数来建立?无论您检查多少元素,下一个元素都可以是关键字。

类似于core.typed这样的类型系统可能能够证明这一点,但基于运行时谓词的断言将无法检查。

现在,除了s/+s/*之外,spec(截至Clojure 1.9.0-alpha14)还有一个名为s/every的组合子,其文档字符串表示:

  

请注意,'every'不会进行详尽的检查,而是采样* coll-check-limit *元素。

所以我们有例如。

(s/valid? (s/* int?) (concat (range 1000) [:foo]))
;= false

(s/valid? (s/every int?) (concat (range 1000) [:foo]))
;= true

(默认*coll-check-limit*值为101)。

这实际上不是对您的示例的立即修复 - 插入s/every代替s/+将无法正常工作,因为每次递归调用都需要验证自己的返回值,这将是涉及实现更多的序列,这将涉及更多的递归调用等。但是您可以将序列构建逻辑分解为没有后置条件的辅助函数,然后让positive-numbers声明后置条件并调用该辅助函数:< / p>

(defn positive-numbers* [n]
  (lazy-seq (cons n (positive-numbers* (inc n)))))

(defn positive-numbers [n]
  {:post [(s/valid? (s/every int? :min-count 1) %)]}
  (positive-numbers* n))

请注意警告:

  1. 这仍然可以实现您的序列中的一大块,这可能会对您的应用程序的性能配置文件造成严重破坏;

  2. 这里唯一的防水保证是,如果seq在位置123456处有一个奇怪的项目,那么实际检查的前缀是所希望的,这将被忽视。

  3. 由于(1),这是一个仅作为测试断言更有意义的东西。 (2)可能是可以接受的 - 你仍然会听到一些愚蠢的错别字,反正的规范的文件值仍然存在;如果它不是,你确实想要一个绝对不透水的保证你的返回类型是所希望的,那么core.typed(也许在本地仅用于少数命名空间)可能是更好的选择。