测试与 类型-Rust版本

几天前, 0xd34df00d发布了该文章的译文,描述了如果我们将某些功能用作“黑匣子”而不是尝试阅读其实现的功能的可能信息。 当然,这些信息因语言而异。 在原始文章中,考虑了四种情况:


  • Python-动态类型,几乎没有来自签名的信息,测试可以得到一些提示;
  • C-弱静态类型,更多信息;
  • Haskell-强静态类型,默认情况下具有纯函数,提供更多信息;
  • Idris-依赖类型,编译器可以证明函数的正确性。

“这里是C,还有Haskell,Rust呢?” -这是以下讨论中的第一个问题。 答复在这里。


让我们首先回顾一下任务:


给定一个值列表和一个值,请返回该值在列表中的索引或表示该值不存在于列表中。

如果不想让所有人都读完这本书, Rust操场上提供了代码示例。
否则,让我们开始吧!



第一种方法是几乎天真的签名,仅在某些惯用元素上与C代码不同:


fn foo(x: &[i32], y: i32) -> Option<usize> { // Implementation elided } 

我们对该功能了解多少? 好吧,事实上-不是很多。 当然,将Option<usize>作为返回值比C提供的功能有了很大的改进,但是没有有关函数语义的信息。 特别是,我们不能保证没有副作用,也无法以某种方式检查所需的行为。


测试可以改善这一点吗? 看这里:


 #[test] fn test() { assert_eq!(foo(&[1, 2, 3], 2), Some(1)); assert_eq!(foo(&[1, 2, 3], 4), None); } 

似乎仅此而已-所有这些检查在Python中可能都是一样的(而且,预期测试对整篇文章没有多大帮助)。


使用通用名称,卢克!


但是仅使用32位带符号的数字有什么好处? 解决方法:


 fn foo<El>(x: &[El], y: El) -> Option<usize> where El: PartialEq, { // Implementation elided } 

好吧,那是什么! 现在我们可以进行任何切片,包括任何可比较类型的元素。 显式多态几乎总是比隐式(hello,Python)好,根本没有多态(hello,C)。


虽然,此函数可能会意外通过此测试:


 fn refl<El: PartialEq + Copy>(el: El) -> Option<usize> { foo(&[el], el) // should always return Some(0), right? } #[test] fn dont_find_nan() { assert_eq!(refl(std::f64::NAN), None); } 

这暗示了一个缺失点,因为规范实际上希望refl函数始终返回Some(0) 。 当然,这全都归因于部分等效类型的一般行为,尤其是浮点数。
也许我们想摆脱这个问题? 因此,我们将简单地限制El类型的界限:


 fn foo<El>(x: &[El], y: El) -> Option<usize> where El: Eq, { // Implementation elided } 

现在,我们不仅要求类型具有可比性,还要求这种比较是等价的 。 当然,这限制了此功能可以使用的类型,但是现在签名和测试都表明行为应符合规范。


旁注:我们想更通用!

这个案例与最初的任务无关,但这似乎是一个众所周知的原则的很好的例子:“对接受的东西持自由态度,对所做的事情持保守态度”。 换句话说:如果您可以在不损害人体工程学和性能的情况下概括输入类型,则可能应该这样做。


现在,我们将检查以下内容:


 fn foo<'a, El: 'a>(x: impl IntoIterator<Item = &'a El>, y: El) -> Option<usize> where El: Eq, { // Implementation elided } 

现在我们对该功能了解多少? 总的来说,它们都是一样的,但是现在它不仅接受切片或列表,还接受一些任意对象,这些对象可以产生对类型El的引用,以便我们将其与所讨论的对象进行比较。 例如,如果我没记错的话,在Java中,这种类型将是Iterable<Comparable>


像以前一样,但是更加严格


但是现在,也许我们需要更多保证。 或者我们想在堆栈上工作(因此不能使用Vec ),但是需要针对每种可能的数组大小归纳我们的代码。 或者我们要编译针对每种具体数组大小优化的函数。


无论如何,我们需要一个通用数组-Rust中有一个板条箱, 正是提供了这一点


现在,这是我们的代码:


 use generic_array::{GenericArray, ArrayLength}; fn foo<El, Size>(x: GenericArray<El, Size>, y: El) -> Option<usize> where El: Eq, Size: ArrayLength<El>, { // Implementation elided } 

我们从中知道什么? 我们知道函数将采用某个特定大小的数组,并在其类型中反映出来(并且将针对每个此类大小独立编译)。 目前,这几乎没有什么-以前的实现在运行时提供了相同的保证。


但是我们可以走得更远。


类型级算术


最初的文章提到了Idris提供的一些保证,而其他语言则无法获得这些保证。 其中之一-可能是最简单的,因为它不涉及任何证明或测试,只是类型略有变化,-声明返回值(如果不是Nothing )将始终小于列表长度。


看起来依赖类型(或类似的类型)对于这种保证是必需的,我们不能从Rust获得相同的信息,对吗?


符合typenum 。 使用它,我们可以通过以下方式编写函数:


 use generic_array::{ArrayLength, GenericArray}; use typenum::{IsLess, Unsigned, B1}; trait UnsignedLessThan<T> { fn as_usize(&self) -> usize; } impl<Less, More> UnsignedLessThan<More> for Less where Less: IsLess<More, Output = B1>, Less: Unsigned, { fn as_usize(&self) -> usize { <Self as Unsigned>::USIZE } } fn foo<El, Size>(x: GenericArray<El, Size>, y: El) -> Option<Box<dyn UnsignedLessThan<Size>>> where El: Eq, Size: ArrayLength<El>, { // Implementation elided } 

“这是什么黑魔法?!” -你可以问。 而且您是对的:typenum 一个黑魔法,任何使用它的尝试都更加神奇。

但是此功能签名是相当具体的。


  • 它需要一组长度为Size的El's和另一个El。
  • 它返回一个Option,如果为Some,
    • 拥有一个基于UnsignedLessThan<Size>特征的特征对象
    • 并且在实现UnsignedIsLess<T>任何地方都实现了Unsigned IsLess<T> ,而IsLess<T>返回B1,即true。

换句话说, 保证此函数返回小于数组大小的无符号整数(严格来说,它返回trait对象,但是我们可以调用as_usize方法来获取整数)。


我现在可以说两个主要警告:


  1. 我们会损失性能。 如果此函数以某种方式位于程序的“热门”路径上,则恒定的动态调度可能会减慢整个过程。 实际上,这可能不是一个大问题,但是还有另一个问题:
  2. 为了编译该函数,我们必须在其内部编写其正确性证明,或者使用一些unsafe欺骗类型系统。 前者相当复杂,而后者只是作弊。

结论


当然,在实践中,我们通常将使用第二种方法(具有通用切片)或扰流器中的方法(具有迭代器)。 随后的所有讨论可能都没有任何实际意义,在这里仅是对类型的练习。


无论如何,就我而言,Rust类型系统可以从更强大的Idris类型系统中模拟功能这一事实本身就给人留下了深刻的印象。

Source: https://habr.com/ru/post/zh-CN468271/


All Articles