Rust1.74で追加された機能を使ってモナドを作ってみる
これは何の記事?
すっかり忘れて遅れてしまいましたが、Rustアドベントカレンダー15日目の記事です!
(何番煎じかわかりませんが)今回はRustでモナドを作ってみようという記事になります。
なんでやろうと思ったの?
最初のきっかけはfuncさんとの会話になります。
以前RustにGATsという機能が導入されたとき、「Rustでもモナドを作ることはできるのか?」ということが話題になりました。GATsはざっくりいうと、trait内の関連型でGenericsを使用できる機能です。(詳しい内容はhttps://blog.rust-lang.org/2022/10/28/gats-stabilization.html を参照ください。)
偉大な先人の方々がモナドの実装を試してくれたのですが、そのときは ApplicativeからMonadを実装する MonadからApplicativeを実装することができなかったようです。そのときはmapの返り値の型を縛ることができないため、Rustがコンパイルエラーを出すことが原因だった様でした。(すみません、一部間違っていました。YOSHIMURAさんご指摘ありがとうございます!)
詳しい説明は以下のブログが詳しいです。
この記事だと問題点として以下のコードが実装できることを問題にしていました。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
となると結局こうなってしまいますね。これだとOption
をfmap
して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
こんな機能が追加されたことを知りませんでした👀 あとで自分も軽くためしてみたいのですが、1点あまり記事の本質ではありませんが、
こちらは反対で「
Monad
からApplicative
を実装することができなかった(impl <M: Monad> Applicative for M
が定義できなかった)」になります。このあたり、表現力の関係と実装できる・できないの関係がややこしいのですがMonad
とApplicative
では、Monad
の方が表現力が上であるMonad
は常にApplicative
であるApplicative
の方がMonad
よりも抽象的」となるなぜこうなる☝️のかというと、
Monad
は「操作Aが完了してからその結果をつかって操作Bを行う」ような逐次的な性質を示すOption<A>
の値がSome
だったら関数f
に突っ込む」みたいなResult
型の関数を?;
でどんどん上から下へ実行していって、途中でErr
になったら止めるみたいなのがよくあると思いますが、まさにあんなイメージです?;
の部分がもしOk
であればその結果を使って次の関数を実行するなどができますApplicative
は(map2
のインターフェースを見ると分かりやすいかも?)2つの計算を独立に実行するということになっている?;
に無理やり対応させるとしたらlet _ = somethig1()?; let _ = something2()?;
👈 こんな感じで結果を常に捨てる縛り(?)でやっているような感じですしたがって順序をつけられる(=
Monad
)のであれば、なんでもいいので何かしらの順番で並べてしまえばApplicative
を満すことができるといった感じになります。ご指摘ありがとうございます!YOSHIMURAさんの記事を読ませていただいて、順番が逆なのに気づいていたのですが修正するのを忘れていました。
こちらも理解しました!MonadはHaskellでいうところの
bind
で順序性を持っていて、Applicativeはそれがないので順序性がない、なのでRustのflat_map
を使って順序性をなくしてApplicativeにしている。という理解をしています。