几天前, 0xd34df00d在此处发布了一篇文章的翻译,其中描述了您可以在不使用有关函数实现信息的情况下将其视为“黑匣子”的情况(但是,当然也要阻止其使用编译器),如果您将其视为“黑匣子”,则可以用不同的语言了解什么。 当然,收到的信息非常依赖于语言-原始文章中考虑了四个示例:
- Python-动态输入的最少信息,只有测试能提供一些提示;
- C-弱静态类型,没有更多信息;
- Haskell-强静态类型化,具有纯函数,更多信息;
- Idris是一种具有依赖类型的语言,在编译过程中有足够的信息证明函数的正确性。
“有C,有Haskell,但Rust在哪里?!” -立即提出问题。 答案是削减。
回顾问题的状况:
让清单和一些含义。 有必要返回该值在列表中的索引或指示该值不在列表中。
对于急躁的人,下面讨论的所有选项都可以在Rust游乐场中看到。
走吧
简单搜索
我们将从一个几乎天真的签名开始,实际上,它仅在某些惯用元素上与C代码不同:
fn foo(x: &[i32], y: i32) -> Option<usize> {
我们对这项功能了解多少? 好吧...实际上不是那么多。 当然,在返回值中使用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, {
是的 这已经是东西了。 现在,我们可以进行切片,其中包含可以比较相等性的任何元素。 显性多态几乎总是比隐性好,几乎总是比没有好。
但是,这样的功能可能会意外地通过以下测试:
fn refl<El: PartialEq + Copy>(el: El) -> Option<usize> { foo(&[el], el)
这立即表明我们有一些缺陷,因为根据原始规范,这样的调用将必须返回Some(0)
。 当然,这里的问题是由于类型的特殊性,通常具有部分定义的比较,尤其是浮点数。
假设现在我们想摆脱这样的问题-为此,我们只收紧对El类型的要求:
fn foo<El>(x: &[El], y: El) -> Option<usize> where El: Eq, {
现在,我们不仅要求进行相等性比较的可能性,还要求这种比较是等价关系 。 这在某种程度上缩小了可能的参数范围,但是现在类型和测试都建议(尽管没有明确指出)预期的行为应该真正落入规范中。
题外话:我们想更通用!此选项与原始任务无关,但我认为是该原则的很好例证:“在接受的内容上保持自由,对所做的事情保持保守”。 换句话说:如果有机会在不影响人体工程学和性能的情况下,使接受的值的类型更加笼统,那么这样做是有意义的。
考虑以下选项:
fn foo<'a, El: 'a>(x: impl IntoIterator<Item = &'a El>, y: El) -> Option<usize> where El: Eq, {
现在我们对该功能了解多少? 一切都是一样的,只是现在它不接受列表或切片作为输入,而是可以使一些任意对象产生,以交替给出与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>, {
我们从这段代码中知道什么? 该函数采用一个固定大小的数组,并在其类型中反映出来(并针对每个此类大小独立编译)。 到目前为止,这并没有太大变化-最终,不仅在同构化阶段而且在运行时,完全相同的保证为以前的版本提供了切入。
但是我们可以走得更远。
类型级别算术
原始文章提到了我们从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>, {
“这黑魔法到底是什么?!” -你问。 您肯定是对的:typenum是那个黑魔法,并且至少在某种程度上合理地尝试使用它是双重的。
但是,此功能的签名是非常明确的。
- 该函数接受一组长度为El的El元素和一个类型为El的元素。
- 该函数返回一个Option值,如果为Some,
- 它是基于
UnsignedLessThan<T>
的特征对象 ,它接受Size类型作为参数; - 反过来,对于实现
Unsigned
和IsLess<T>
所有类型IsLess<T>
实现了UnsignedLessThan<T>
IsLess<T>
, IsLess<T>
返回的所有类型都为B1,即 是真的
换句话说,以这种方式,我们编写了一个函数,该函数保证返回小于数组原始大小的非负(无符号)数字(或者,当然,它返回相同的特征对象,我们稍后必须as_usize
调用as_usize
方法,保证返回该数字) 。
此方法有两个技巧:
- 我们可能会明显损失性能。 如果由于某种原因突然使我们的功能出现在程序的“热门”部分中,则对动态调用的不断需求可能是最慢的操作之一。 但是,此缺点可能看起来并不那么严重,但还有第二个缺点:
- 为了正确编译该函数,我们将需要在其中实际编写其工作正确性的证明,或者通过
unsafe
“欺骗”类型系统。 对于星期五的文章,第一个太复杂了,但是第二个仅仅是一个骗局。
结论
当然,实际上,在这种情况下,将使用第二种实现方式(接收任意类型的切片)或扰流器下的实现方式(接收可迭代对象)。 几乎所有随后的论点肯定都没有任何实际意义,而只是作为使用类型系统的练习。
不过,在我看来,Rust类型系统可以模仿明显更强的Idris类型系统的功能之一这一事实,在我看来是很有指示性的。