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 中構建我們需要的類型:
type building haskell
同時我們將如何在 Rust 中做到這一點:
type building in rust
管理特徵:Generic1 和 Plug
爲了促進上述類型的構建 / 破壞,我們需要兩個具有關聯類型的管理特徵(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
有了類型管理特徵,我們可以很容易地定義和實現 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)),
}
}
}
Applicative
這個可以說是最難看的。爲清楚起見,讓我還爲你提供更合理的 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
最後是 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/
參考資料
[1]
文章: https://gist.github.com/edmundsmith/855fcf0cb35dd467c29a9350481f0ecf
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/2QE3Ljy2tVXp6FhK0Bi06w