测试还是类型? -锈版

几天前, 0xd34df00d在此处发布了一篇文章的翻译,其中描述了您可以在不使用有关函数实现信息的情况下将其视为“黑匣子”的情况(但是,当然也要阻止其使用编译器),如果您将其视为“黑匣子”,则可以用不同的语言了解什么。 当然,收到的信息非常依赖于语言-原始文章中考虑了四个示例:


  • Python-动态输入的最少信息,只有测试能提供一些提示;
  • C-弱静态类型,没有更多信息;
  • Haskell-强静态类型化,具有纯函数,更多信息;
  • Idris是一种具有依赖类型的语言,在编译过程中有足够的信息证明函数的正确性。

“有C,有Haskell,但Rust在哪里?!” -立即提出问题。 答案是削减。


回顾问题的状况:


让清单和一些含义。 有必要返回该值在列表中的索引或指示该值不在列表中。

对于急躁的人,下面讨论的所有选项都可以在Rust游乐场中看到。
走吧


简单搜索


我们将从一个几乎天真的签名开始,实际上,它仅在某些惯用元素上与C代码不同:


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

我们对这项功能了解多少? 好吧...实际上不是那么多。 当然,在返回值中使用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, { // 10000    } 

是的 这已经是东西了。 现在,我们可以进行切片,其中包含可以比较相等性的任何元素。 显性多态几乎总是比隐性好,几乎总是比没有好。


但是,这样的功能可能会意外地通过以下测试:


 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); } 

这立即表明我们有一些缺陷,因为根据原始规范,这样的调用将必须返回Some(0) 。 当然,这里的问题是由于类型的特殊性,通常具有部分定义的比较,尤其是浮点数。
假设现在我们想摆脱这样的问题-为此,我们只收紧对El类型的要求:


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

现在,我们不仅要求进行相等性比较的可能性,还要求这种比较是等价关系 。 这在某种程度上缩小了可能的参数范围,但是现在类型和测试都建议(尽管没有明确指出)预期的行为应该真正落入规范中。


题外话:我们想更通用!

此选项与原始任务无关,但我认为是该原则的很好例证:“在接受的内容上保持自由,对所做的事情保持保守”。 换句话说:如果有机会在不影响人体工程学和性能的情况下,使接受的值的类型更加笼统,那么这样做是有意义的。


考虑以下选项:


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

现在我们对该功能了解多少? 一切都是一样的,只是现在它不接受列表或切片作为输入,而是可以使一些任意对象产生,以交替给出与El类型的对象的链接,并将它们与所追捧的对象进行比较:如果我没记错的话,Java中的模拟是将是一个带有Iterable<Comparable>的函数。


和以前一样,只有更严格


但是,例如,编译器在已知阶段提供的保证对我们来说还不够。 或者说,我们不希望(出于某种原因或其他原因)进入堆,而是希望在堆栈上工作,这意味着我们需要一个数组而不是一个向量,但同时我们希望我们的代码可以泛化为不同大小的数组。 或者我们希望针对输入列表的每个特定大小尽可能优化该功能。

简而言之,我们需要一个通用数组-Rust已经有了一个提供逐字记录的软件包。


现在我们可以使用以下代码:


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

我们从这段代码中知道什么? 该函数采用一个固定大小的数组,并在其类型中反映出来(并针对每个此类大小独立编译)。 到目前为止,这并没有太大变化-最终,不仅在同构化阶段而且在运行时,完全相同的保证为以前的版本提供了切入。


但是我们可以走得更远。


类型级别算术


原始文章提到了我们从Idris获得的一些保证,而这些保证是其他任何人都无法获得的。 其中一个-也许是最简单的一个,因为为此,您甚至不需要编写完整的证明或完整的测试,而只需指定一点类型,它表示返回值(如果存在)(即,如果不是,则为Nothing ),以确保不超过输入列表的长度。

看起来这样保证的必要条件是依赖类型的存在,或者至少是某种相似性,而从Rust期望这样的事情是很奇怪的,对吧?


Meet- 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>, { // 10000    } 

“这黑魔法到底是什么?!” -你问。 您肯定是对的:typenum是那个黑魔法,并且至少在某种程度上合理地尝试使用它是双重的。

但是,此功能的签名是非常明确的。


  • 该函数接受一组长度为El的El元素和一个类型为El的元素。
  • 该函数返回一个Option值,如果为Some,
    • 它是基于UnsignedLessThan<T>特征对象 ,它接受Size类型作为参数;
    • 反过来,对于实现UnsignedIsLess<T>所有类型IsLess<T>实现了UnsignedLessThan<T> IsLess<T>IsLess<T>返回的所有类型都为B1,即 是真的

换句话说,以这种方式,我们编写了一个函数,该函数保证返回小于数组原始大小的非负(无符号)数字(或者,当然,它返回相同的特征对象,我们稍后必须as_usize调用as_usize方法,保证返回该数字) 。


此方法有两个技巧:


  1. 我们可能会明显损失性能。 如果由于某种原因突然使我们的功能出现在程序的“热门”部分中,则对动态调用的不断需求可能是最慢的操作之一。 但是,此缺点可能看起来并不那么严重,但还有第二个缺点:
  2. 为了正确编译该函数,我们将需要在其中实际编写其工作正确性的证明,或者通过unsafe “欺骗”类型系统。 对于星期五的文章,第一个太复杂了,但是第二个仅仅是一个骗局。

结论


当然,实际上,在这种情况下,将使用第二种实现方式(接收任意类型的切片)或扰流器下的实现方式(接收可迭代对象)。 几乎所有随后的论点肯定都没有任何实际意义,而只是作为使用类型系统的练习。


不过,在我看来,Rust类型系统可以模仿明显更强的Idris类型系统的功能之一这一事实,在我看来是很有指示性的。

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


All Articles