Closed12

Rustでモナド

yukiyuki

Rust ではこれまでモナドが実装できなかった(かなりがんばらないと; ので、Rust にはいわゆる"モナド"はなかった)。

が、nightly で GATs が導入されてそれらしいものを実装できるようになった。

pub(crate) trait Functor {
    type A;
    type Lifted<B>: Functor;

    fn map<F, B>(self, f: F) -> Self::Lifted<B>
    where
        F: FnMut(Self::A) -> B;
}

pub(crate) trait Pointed: Functor {
    fn pure<T>(t: T) -> Self::Lifted<T>;
}

pub(crate) trait Applicative: Pointed {
    fn apply<F, B, C>(self, b: Self::Lifted<B>, f: F) -> Self::Lifted<C>
    where
        F: FnMut(Self::A, B) -> C;
}

pub(crate) trait Monad: Applicative {
    fn bind<B, F>(self, f: F) -> Self::Lifted<B>
    where
        F: FnMut(Self::A) -> Self::Lifted<B>;
}

これを適当なデータ型(たとえば Identity)に適用でき、実装を追加すればモナドにすることができる。

struct Id<M>(M);

impl<A> Monad for Id<A> {
    fn bind<B, F>(self, mut f: F) -> Id<B>
    where
        F: FnMut(A) -> Id<B>,
    {
        f(self.0)
    }
}

impl<A> Pointed for Id<A> {
    fn pure<T>(t: T) -> Self::Lifted<T> {
        Id(t)
    }
}

impl<A> Applicative for Id<A> {
    fn apply<F, B, C>(self, b: Id<B>, mut f: F) -> Id<C>
    where
        F: FnMut(A, B) -> C,
    {
        Id(f(self.0, b.0))
    }
}

impl<A> Functor for Id<A> {
    type A = A;
    type Lifted<B> = Id<B>;

    fn map<F, B>(self, mut f: F) -> Id<B>
    where
        F: FnMut(A) -> B,
    {
        Id(f(self.0))
    }
}
yukiyuki

アドベントカレンダーでこの話をちょっと書こうと思っているので、いくつかメモを残す!

yukiyuki

モナドは do 記法(や Scala の for-yield)のように宣言的な記法を可能にする構文とセットにして DSL にするところに醍醐味があると思うので、ちょっとそれをやってみる。

yukiyuki

GATs のモチベーション↓
https://github.com/rust-lang/rfcs/blob/master/text/1598-generic_associated_types.md

Generic Associated Types。

RFC を読んでいる感じだと、高階型導入のための前段として入れることを意図しているらしい(ただし RFC 自体は2017年で、当時とユーザー層が変わっていて、現在もユーザーの多くから所望されているかはわからないけど)。

もともとのモチベーションを少し読み解いていく。

ライフタイムパラ―メータを持つ関連型を定義できるようにしたい

下記は代表的な GAT の利用例。こうした型はたまに欲しくなると思うけれど、現状の stable な Rust コンパイラでは通らない。

trait StreamingIterator {
    type Item<'a>;
    fn next<'a>(&'a mut self) -> Option<Self::Item<'a>>;
}

ジェネリクス(Rust だとジェネリックと翻訳される?)を持つ関連型を定義できるようにしたい

これはよく欲しくなると思うけれど、ジェネリクスを型情報に持つ型を関連型に定義し、関連型側の指定でジェネリクスを切り替えられるようにしたい場合。これも、現状の stable な Rust コンパイラでは通らない。

これを実現したい場合、トレイトのにジェネリクスをもたせて関連型の右辺の型をジェネリクスに対応させるみたいなことをして以前回避したような気がする。気がするだけかも。

trait PointerFamily {
    type Pointer<T>: Deref<Target = T>;
    fn new<T>(value: T) -> Self::Pointer<T>;
}

struct ArcFamily;

impl PointerFamily for ArcFamily {
    type Pointer<T> = Arc<T>;
    fn new<T>(value: T) -> Self::Pointer<T> {
        Arc::new(value)
    }
}

struct RcFamily;

impl PointerFamily for RcFamily {
    type Pointer<T> = Rc<T>;
    fn new<T>(value: T) -> Self::Pointer<T> {
        Rc::new(value)
    }
}

struct Foo<P: PointerFamily> {
    bar: P::Pointer<String>,
}
yukiyuki

かなり雑な例だけど bind 動いた。(Id モナドに #[derive(Debug)] も裏で追加しています)

main.rs
#![feature(generic_associated_types)]

use identity::*;
use monad::Monad;

fn main() {
    let i1 = Id(1);
    let i2 = i1.bind(|i| Id(i + 1));
    let i3 = i2.bind(|i| Id(i + 1));

    println!("{:?}", i3);
}
yukiyuki

pure のシグネチャが結構微妙かも。pure に用意されている型パラメータと Id が保持する型パラメータが一致していなくて、ターボフィッシュが必要になってしまっている。ちょっと実装間違えたかな。

main.rs
#![feature(generic_associated_types)]

use functor::Pointed;
use identity::*;
use monad::Monad;

fn main() {
    let i1 = Id(1);
    let i2 = i1.bind(|i| Id::<i32>::pure(i + 1));
    let i3 = i2.bind(|i| Id::<i32>::pure(i + 1));

    println!("{:?}", i3);
}
yukiyuki

やっぱ実装間違ってた(たぶん参考にした元記事も間違ってる)。

Pointedpure には独自の型パラメータは必要ない。Functor の持つ A を使用したらいい。Functor を下記のように直すだけで、

functor/mod.rs
pub(crate) trait Functor {
    type A;
    type Lifted<B>: Functor;

    fn map<F, B>(self, f: F) -> Self::Lifted<B>
    where
        F: FnMut(Self::A) -> B;
}

pub(crate) trait Pointed: Functor {
    fn pure(t: Self::A) -> Self::Lifted<Self::A>;
}

pub(crate) trait Applicative: Pointed {
    fn apply<F, B, C>(self, b: Self::Lifted<B>, f: F) -> Self::Lifted<C>
    where
        F: FnMut(Self::A, B) -> C;
}

main の pure の呼び出しもスッキリさせられる。

main.rs
#![feature(generic_associated_types)]

use functor::Pointed;
use identity::*;
use monad::Monad;

fn main() {
    let i1 = Id(1);
    let i2 = i1.bind(|i| Id::pure(i + 1));
    let i3 = i2.bind(|i| Id::pure(i + 1));

    println!("{:?}", i3);
}
yukiyuki

MonadError を用意していくとこんな感じになるかな?ちょっと使うかわからないので型クラスだけ適当に置いておいて放置します🙏 for-yield のような制御構造を使用する際には必須になりそうなので一応用意。

err/mod.rs
use crate::functor::Applicative;

use super::Monad;

trait ApplicativeError: Applicative {
    type Err;

    fn raise_error(self, e: Self::Err) -> Self::Lifted<Self::A>;
}

trait MonadError: ApplicativeError + Monad {
    fn ensure<F>(self, e: Self::Err, predicate: F) -> Self::Lifted<Self::A>
    where
        F: FnMut(Self::A) -> bool;
}
yukiyuki

do notation についてはこのシリーズを真似させてもらおうかな。

ざっくりまとめると、

  • do! マクロで do 記法を開始できる。
  • let a! = ...a <- ... を再現できる。
  • return!(...)return ... を再現できる。
  • 内部は ControlFlow という enum で表現されていて、do! マクロは実質 ControlFlow のシンタックスシュガーになっている。

https://varkor.github.io/blog/2018/11/10/monadic-do-notation-in-rust-part-i.html

つまりマクロを使ってなんとかする感じなのか。それはそうという気はする。

yukiyuki

こんな感じのマクロを作れたら最高かなあ。

右辺がきちんと Monad を実装しているかを確認して、実装されていれば bind をして、bind 時の値を取り出す。それを2回繰り返して、yield 時には Functor を確認して map が実装されているかを確認し、実装されていれば yield の後ろの関数を実行するイメージ。

    do! {
        i1 <- Id::pure(1)
        i2 <- Id::pure(2)
        return i1 + i2
    }
このスクラップは2020/12/17にクローズされました