如何指定惰性序列生成函数?
how to spec a lazy-seq generating function?
我希望在生成器函数的预条件和 post 条件中使用 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 来描述生成器函数的 :post
结果的方法(不会导致堆栈溢出)?
理论上正确的答案是,通常您无法检查惰性序列是否与规范匹配,而无需全部实现。
在您的 (s/+ int?)
的具体示例中,给定一个惰性序列,如何仅通过观察序列来确定其所有元素是否都是整数?无论您检查多少元素,下一个元素始终可能是关键字。
这是类型系统,比如 core.typed 可能能够证明的事情,但是 runtime-predicate-based 断言将无法检查。
现在,除了 s/+
和 s/*
,规范(自 Clojure 1.9.0-alpha14 起)还有一个名为 s/every
的组合器,其文档字符串是这样说的:
Note that 'every' does not do exhaustive checking, rather it samples *coll-check-limit* elements.
所以我们有例如
(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/+
是行不通的,因为每个递归调用都需要验证自己的 return 值,这将涉及实现更多的序列,这将涉及更多的递归调用等。但是您可以将 sequence-building 逻辑分解为没有后置条件的辅助函数,然后让 positive-numbers
声明后置条件并调用辅助函数:
(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))
注意注意事项:
这仍然会实现您的序列的很大一部分,这可能会对您的应用程序的性能配置文件造成严重破坏;
这里唯一的水密保证是实际检查的前缀是符合要求的,如果seq在123456位置有一个奇怪的项目,那将不会被注意到。
由于 (1),这作为 test-only 断言更有意义。 (2) 可能是可以接受的——你仍然会发现一些愚蠢的拼写错误,而且规范的文档价值还是存在的;如果不是,并且您确实希望绝对无懈可击地保证您的 return 类型符合要求,那么 core.typed(可能在本地仅用于少数名称空间)可能是更好的选择。
我希望在生成器函数的预条件和 post 条件中使用 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 来描述生成器函数的 :post
结果的方法(不会导致堆栈溢出)?
理论上正确的答案是,通常您无法检查惰性序列是否与规范匹配,而无需全部实现。
在您的 (s/+ int?)
的具体示例中,给定一个惰性序列,如何仅通过观察序列来确定其所有元素是否都是整数?无论您检查多少元素,下一个元素始终可能是关键字。
这是类型系统,比如 core.typed 可能能够证明的事情,但是 runtime-predicate-based 断言将无法检查。
现在,除了 s/+
和 s/*
,规范(自 Clojure 1.9.0-alpha14 起)还有一个名为 s/every
的组合器,其文档字符串是这样说的:
Note that 'every' does not do exhaustive checking, rather it samples *coll-check-limit* elements.
所以我们有例如
(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/+
是行不通的,因为每个递归调用都需要验证自己的 return 值,这将涉及实现更多的序列,这将涉及更多的递归调用等。但是您可以将 sequence-building 逻辑分解为没有后置条件的辅助函数,然后让 positive-numbers
声明后置条件并调用辅助函数:
(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))
注意注意事项:
这仍然会实现您的序列的很大一部分,这可能会对您的应用程序的性能配置文件造成严重破坏;
这里唯一的水密保证是实际检查的前缀是符合要求的,如果seq在123456位置有一个奇怪的项目,那将不会被注意到。
由于 (1),这作为 test-only 断言更有意义。 (2) 可能是可以接受的——你仍然会发现一些愚蠢的拼写错误,而且规范的文档价值还是存在的;如果不是,并且您确实希望绝对无懈可击地保证您的 return 类型符合要求,那么 core.typed(可能在本地仅用于少数名称空间)可能是更好的选择。