GAT 懶人包

Wu Yu Wei published on
5 min, 850 words

Rust 1.65 提供了大家期待已久的功能「Generic Associated Types」,這是在 Functional Programming 中 Higher-Kind Types 的其中一種實現。對於已經有這樣需求的人來說,這些概念應該並不陌生,或是早已知道如何善用它們了。但對於從未接觸過的人來說,我想應該就會遇到一些困難。通常看到新出的 Rust 進階功能,我們都會推薦大家去看 RFC,而我近日也將 GAT 的 RFC 翻譯完畢。但我並沒有相當的把握確保新手能了解文中舉的例子「StreamingIterator」,因為它的泛型用到了生命週期,尤其是後面又繼續提到了各種進階的用法,像是怎麼與 Higher-Rank Trait Bounds 一起使用等等。我有點擔心新手一開始接觸時就會看的眼花撩亂。這次我們不妨把所有概念都拋諸腦後,看看在寫程式的過程中,到底為何會遇上這樣的需求。

讓我們用各型別都有的方法 map 來作為例子,這在 OptionResult 都有。如果今天我們想定義一個 Trait 將該方法抽象化成泛型(generic)型別的話,我們會發現這是做不到的!其中一個得回傳 Option<T> 而另一個得回傳 Result<T>。這時你可能就會想將 OptionResult 作為泛型的引數(argument),但是這也是做不到的!泛型型別的名稱在 Rust 不是一個基礎的型別!

而 GAT 能夠允許你達成這樣的事,這也是為何 RFC 一開始得先介紹 Kind 以及 Type Constructor 等概念,我非常推薦有時間可以讀一讀這段。不過畢竟這篇是懶人包,怕太難懂我們可以直接先來看看真正的例子會怎麼寫。我們可以宣告一個這樣的 trait:

trait Mappable {
    type Item;
    type Target<U>;
    fn map<U, F: FnMut(Self::Item) -> U>(self, f: F) -> Self::Target<U>;
}

然後針對 OptionResult 實作它們:

impl<T> Mappable for Option<T> {
    type Item = T;
    type Target<U> = Option<U>;
    fn map<U, F: FnMut(Self::Item) -> U>(self, f: F) -> Option<U> {
        self.map(f)
    }
}

impl<T, E> Mappable for Result<T, E> {
    type Item = T;
    type Target<U> = Result<U, E>;
    fn map<U, F: FnMut(Self::Item) -> U>(self, f: F) -> Result<U, E> {
        self.map(f)
    }
}

這樣一來你就能使用「泛型的泛型」來寫出這樣的函數:

fn absolute<M: Mappable<Item = i32>>(m: M) -> <M as Mappable>::Target<u32> {
    m.map(|x| x.abs() as u32)
}

這還能用在 Vec 上,完整的範例可以參考 Rust Playground 的連結。在 RFC 中稱這樣的模式為「Family」模式,同樣的作法也能套用在指標的泛型上,像是 ArcRc。當然 GAT 也可以用在生命週期上,就如同 RFC 上的 StreamingIterator,而這種 trait 的用途就是希望能只可變借用到特定片段的切片,而非平常 IterMut 得借用一整個集合。而在往下還有更多複雜的用法,不過希望看完這篇懶人包你能對 GAT 有初步的了解,在閱讀 RFC 時就不會那麼令人望之卻步了。