Ⓜ️

Rust1.74で追加された機能を使ってモナドを作ってみる

2023/12/17に公開
2

これは何の記事?

すっかり忘れて遅れてしまいましたが、Rustアドベントカレンダー15日目の記事です!
(何番煎じかわかりませんが)今回はRustでモナドを作ってみようという記事になります。

なんでやろうと思ったの?

最初のきっかけはfuncさんとの会話になります。
https://x.com/func_hs/status/1726045859132109185?s=20

以前RustにGATsという機能が導入されたとき、「Rustでもモナドを作ることはできるのか?」ということが話題になりました。GATsはざっくりいうと、trait内の関連型でGenericsを使用できる機能です。(詳しい内容はhttps://blog.rust-lang.org/2022/10/28/gats-stabilization.html を参照ください。)

偉大な先人の方々がモナドの実装を試してくれたのですが、そのときは ApplicativeからMonadを実装する MonadからApplicativeを実装することができなかったようです。そのときはmapの返り値の型を縛ることができないため、Rustがコンパイルエラーを出すことが原因だった様でした。(すみません、一部間違っていました。YOSHIMURAさんご指摘ありがとうございます!)

詳しい説明は以下のブログが詳しいです。
https://jackh726.github.io/rust/2022/05/04/a-shiny-future-with-gats.html
https://zenn.dev/yyu/articles/f60ed5ba1dd9d5
https://blog-dry.com/entry/2020/12/25/130250

この記事だと問題点として以下のコードが実装できることを問題にしていました。fmapの返り値がOptionでなくResultという変な挙動ができてしまうというわけです。

impl<T> Functor for Option<T> {
    type Inner = T;
    type This<B> = Result<B, ()>;

    fn fmap<F, B>(self, f: F) -> Self::This<B>
    where
        F: FnOnce(Self::Inner) -> B {
        self.map(f).ok_or(())
    }
}

これがRust1.74の新しい機能で解決できるのでは?というのが今回の記事の目的になります。

Rust1.74で何ができる様になったの?

Rust1.74の機能で、async fn-> impl Traitの返り値にもSelfのような不透明な型を使える様になりました。今後この記事では『impl trait projections』 と呼びます。

Announcing Rust 1.74.0から例を持ってくると以下の様な感じになります。trait内で使用できる型の自由度が広がったことで、traitがより使いやすくなりました。

struct Wrapper<'a, T>(&'a T);

// Opaque return types that mention `Self`:
impl Wrapper<'_, ()> {
    async fn async_fn() -> Self { /* ... */ }
    fn impl_trait() -> impl Iterator<Item = Self> { /* ... */ }
}

trait Trait<'a> {
    type Assoc;
    fn new() -> Self::Assoc;
}
impl Trait<'_> for () {
    type Assoc = ();
    fn new() {}
}

// Opaque return types that mention an associated type:
impl<'a, T: Trait<'a>> Wrapper<'a, T> {
    async fn mk_assoc() -> T::Assoc { /* ... */ }
    fn a_few_assocs() -> impl Iterator<Item = T::Assoc> { /* ... */ }
}

で、これを使って型を縛れるか?というのが今回の検証内容になります。

検証する

まず、Functorのtraitを定義しましょう。

impl<T> Functor for Option<T> {
    type Inner = T;
    type This<B> = Option<B>;

    fn fmap<F, B>(self, f: F) -> impl Functor<Inner = B, This = Self::This<B>>
    where
        F: FnOnce(Self::Inner) -> B,
    {
        self.map(f).ok_or(())
    }
}

これだとコンパイルエラーになります。traitでは->impl Trait使えないからダメみたいですね。

`impl Trait` only allowed in function and inherent method return types, not in trait method return types
see issue #91611 <https://github.com/rust-lang/rust/issues/91611> for more informationrustcClick for full compiler diagnostic)

Traitをこの様にして、

impl<T> Functor for Option<T> {
    type Inner = T;
    type This<B> = Result<B, ()>;

    fn fmap<F, B>(self, f: F) -> Self::This<B>
    where
        F: FnOnce(Self::Inner) -> B,
    {
        self.map(f).ok_or(())
    }
}

traitをimplする側で-> impl Functorにしてみてはどうでしょうか?

impl<T> Functor for Option<T> {
    type Inner = T;
    type This<B> = Option<B>;

    fn fmap<F, B>(self, f: F) -> impl Functor<Inner = B, This = Self::This<B>>
    where
        F: FnOnce(Self::Inner) -> B,
    {
        self.map(f).ok_or(())
    }
}

これもコンパイルエラーでますね。impl Trait内で-> impl Traitすることは出来なさそうです。

`impl Trait` only allowed in function and inherent method return types, not in `impl` method return types
see issue #91611 <https://github.com/rust-lang/rust/issues/91611> for more informationrustcClick for full compiler diagnostic

となると結局こうなってしまいますね。これだとOptionfmapしてResultを返すことができてしまうので、正しく型を縛ることは出来なさそうです。

struct OptionF<T>(Option<T>);

impl<T> OptionF<T> {
    fn fmap<F, B>(self, f: F) -> impl Functor<Inner = B, This<B> = Result<B, ()>>
    where
        F: FnOnce(T) -> B,
    {
        self.0.map(f).ok_or(())
    }
}

結果と考察

ということで、今の時点の結論です。

  • GATsの時にあったfmapの返り値の型を違うものにできてしまう問題は解決していない。ここは変わらない。
  • trait側やtraitをimplするところでこの機能を使うことはできない。なので型を縛れない。
  • モナドの動作をエミュレートすることはできる。(これは前から)

実装はここに置いてますので、ご参考ください。

最後に

まだ型やモナドについてそこまで詳しい訳ではないので、ツッコミどころも色々あると思います。
何か質問や指摘あればやさしくコメントいただけると嬉しいです!

Discussion

YOSHIMURA YuuYOSHIMURA Yuu

こんな機能が追加されたことを知りませんでした👀 あとで自分も軽くためしてみたいのですが、1点あまり記事の本質ではありませんが、

そのときはApplicativeからMonadを実装することができなかったようです。

こちらは反対で「MonadからApplicativeを実装することができなかった(impl <M: Monad> Applicative for Mが定義できなかった)」になります。このあたり、表現力の関係と実装できる・できないの関係がややこしいのですが

  • MonadApplicativeでは、Monadの方が表現力が上である
    • 任意のMonadは常にApplicativeである
  • 同じ意味で逆の(?)言い方をするなら「Applicativeの方がMonadよりも抽象的」となる

なぜこうなる☝️のかというと、

  • Monadは「操作Aが完了してからその結果をつかって操作Bを行う」ような逐次的な性質を示す
    • たとえば「Option<A>の値がSomeだったら関数fに突っ込む」みたいな
    • RustだとResult型の関数を?;でどんどん上から下へ実行していって、途中でErrになったら止めるみたいなのがよくあると思いますが、まさにあんなイメージです
      • 計算に順序があるので、?;の部分がもしOkであればその結果を使って次の関数を実行するなどができます
  • 一方でApplicativeは(map2のインターフェースを見ると分かりやすいかも?)2つの計算を独立に実行するということになっている
    • 超強引な例かもしれませんが、こっちはある結果を別に使えないので?;に無理やり対応させるとしたらlet _ = somethig1()?; let _ = something2()?;👈 こんな感じで結果を常に捨てる縛り(?)でやっているような感じです
      • このような結果を捨てても「最後までいったか」それとも「途中でエラーになって止ったか」は得ることができます

したがって順序をつけられる(= Monad)のであれば、なんでもいいので何かしらの順番で並べてしまえばApplicativeを満すことができるといった感じになります。

フラワーフラワー

ご指摘ありがとうございます!YOSHIMURAさんの記事を読ませていただいて、順番が逆なのに気づいていたのですが修正するのを忘れていました。

したがって順序をつけられる(= Monad)のであれば、なんでもいいので何かしらの順番で並べてしまえばApplicativeを満すことができるといった感じになります。

こちらも理解しました!MonadはHaskellでいうところのbindで順序性を持っていて、Applicativeはそれがないので順序性がない、なのでRustのflat_mapを使って順序性をなくしてApplicativeにしている。という理解をしています。