关注「Rust编程指北」,一起学习 Rust,给未来投资
大家好,我是螃蟹哥。
本文分享一篇关于 Rust 中「高级」类型的文章。
最近开始学习 Rust。在用 C++ 编写 vulkan raytracer 的过程中,我非常厌倦它乏味的内存管理、头文件/源代码重复以及最糟糕的 IDE 工具,我不得不找到更好的东西。Rust 一直是我周围喋喋不休的常见话题,所以我决定看看,为什么他们讨论这么激烈。
我注意到的第一件事是 Rust 工具非常有效且易于设置。我以前从来没有花这么少的时间为我的 Neovim 设置添加一门新的语言。
Hello world 和打印一些质数很快。因此准备尝试处理一些引起我兴趣的功能:关联类型。Rust 是 Haskell 和 C++ 中最好的吗?
我必须首先提到Edmund Smith 的这篇文章[1],我从他那里窃取了完成这项工作所需的一些核心特征。但我相信最终我得到了一个更简单、更优雅的解决方案,因此我认为这篇文章为讨论增添了一些东西。
我们需要意识到的第一件事是 Functor 不是在传统的“完整”类型上定义的。相反,它是在类型为 * -> * 的类型上定义的。一个例子是 Maybe,或者像俄罗斯人那样称呼它:Option。看一下 Haskell 中 Functor 的定义:
class Functor f where
fmap :: (a -> b) -> f a -> f b
我们期望 f 是仍然可以应用另一种类型的类型。这是我们的第一个问题,我们在 Rust 中缺乏这种能力。虽然有添加此功能的建议,但似乎没有理由怀疑此功能会与核心语言原则发生冲突。
但是,目前我们需要自己破解它。从 Edmund 复制,我决定了两个特性,它们只实现我们的目的所需的关联类型魔法。我们实际上不需要未应用的通用 Option 类型,只要我们可以将 Option<A>
转换为 Option<B>
。
为了比较,以下是我们如何在 haskell 中构建我们需要的类型:
同时我们将如何在 Rust 中做到这一点:
为了促进上述类型的构建/破坏,我们需要两个具有关联类型的管理特征(administrative traits)。用户需要在他们的类型上实现这些 trait,然后他们才能开始实现在更高级的类型上运行的任何其他 trait。
第一个是 Generic1,它允许我们从 Option<A>
中取出 A,或者更确切地说是类型 (I)nside:
trait Generic1 {
type I;
}
impl<A> Generic1 for Option<A> {
type I = A;
}
其次,我们需要一种方法来替换 Option 的内部类型。我们使用 trait Plug 来做到这一点:
trait Plug<B> {
type R;
}
impl<A, B> Plug<B> for Option<A> {
type R = Option<B>;
}
// In other words
// <Option<A> as Plug<B>>::R == Option<B>
请注意,我们必须相信这里的用户会正确实现这些特征。但可悲的是,类型检查器现在完全没有意识到结果类型仍然是 Option 的一个实例,因此通常无法导出表达式的类型。
有了类型管理特征,我们可以很容易地定义和实现 Functor 特征(trait),尽管在语法上有点混乱。
trait Functor {
fn fmap<B>(&self, f: &dyn Fn(&<Self as Generic1>::I) -> B) -> <Self as Plug<B>>::R
where Self: Plug<B> + Generic1;
}
impl<A> Functor for Option<A> {
fn fmap<B>(&self, f: &dyn Fn(&<Self as Generic1>::I) -> B) -> <Self as Plug<B>>::R {
// Apply the function over the contained value, if there is one
match self {
None => None,
Some(v) => Some(f(v)),
}
}
}
这个可以说是最难看的。为清楚起见,让我还为你提供更合理的 ap 函数的 Haskellian 类型签名:
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
而 Rust 的代码:
trait Applicative {
fn ap<A, B, F: Fn(&A) -> B>(x: &<Self as Plug<F>>::R, arg: &<Self as Plug<A>>::R) -> <Self as Plug<B>>::R
where Self: Plug<A> + Plug<B> + Plug<F>;
fn pure(x: Self::I) -> Self
where Self: Generic1;
}
impl<A> Applicative for Option<A> {
fn pure(x: <Self as Generic1>::I) -> Self {
Some(x)
}
fn ap<A2,B,F: Fn(&A2) -> B>(x: &<Self as Plug<F>>::R, arg: &<Self as Plug<A2>>::R) -> <Self as Plug<B>>::R {
match (x, arg) {
(None, _) => None,
(_, None) => None,
(Some(f), Some(v)) => Some(f(v))
}
}
}
最后是 Monad,在 Option 的情况下,这与 Functor 非常相似。唯一的区别是不需要将结果包装在 Some 中,因为现在是提供的函数的责任。
trait Monad {
fn bind<B>(&self, f: &dyn Fn(&Self::I) -> Self::R) -> Self::R
where Self: Plug<B> + Generic1;
}
impl<A> Monad for Option<A> {
fn bind<B>(&self, f: &dyn Fn(&<Self as Generic1>::I) -> <Self as Plug<B>>::R) -> <Self as Plug<B>>::R {
match self {
None => None,
Some(v) => f(v),
}
}
}
让我们看看是否真的可以使用我们的新特征(traits)编写一些代码。请注意,以下代码段在作为有效的 Rust 程序的同时具有尽可能少的类型签名。
fn main() {
let x = Some(5);
let y = x.fmap(&|i| i+1);
let z = x.bind(&|_| Applicative::pure(String::from("bound!")));
let x2 : Option<i32>= None;
let y2 = x2.fmap(&|i| i+1);
let z2 = x2.bind(&|_| Applicative::pure(String::from("bound!")));
let g: Option<&dyn Fn(&i32) -> String> = Some(&|i| format!("{}={}", i, i));
let g_eval = <Option<&dyn Fn(&i32) -> String> as Applicative>::ap(&g, &Some(69));
println!("x: {:?}, x2: {:?}, y: {:?}, y2: {:?}, z: {:?}, z2: {:?}, geval: {:?}", x, x2, y, y2, z, z2, g_eval);
}
运行得到以下输出:
x: Some(5), x2: None, y: Some(6), y2: None, z: Some("bound!"), z2: None, geval: Some("69=69")
所以,你可以在 Rust 中使用更高级的类型。但考虑到实现的语法噪音、经常混淆并需要帮助的类型检查器以及没有多大意义的错误消息,对于大多数项目来说,它肯定不是一个非常有吸引力的选择。特别是 Rust 的错误处理挺不错的。
原文链接:https://hugopeters.me/posts/14/
文章: https://gist.github.com/edmundsmith/855fcf0cb35dd467c29a9350481f0ecf
推荐阅读
觉得不错,点个赞吧
扫码关注「Rust编程指北」