Rustでモナド
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))
}
}
参考記事↓
アドベントカレンダーでこの話をちょっと書こうと思っているので、いくつかメモを残す!
モナドは do 記法(や Scala の for-yield)のように宣言的な記法を可能にする構文とセットにして DSL にするところに醍醐味があると思うので、ちょっとそれをやってみる。
GATs のモチベーション↓
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>,
}
かなり雑な例だけど bind
動いた。(Id モナドに #[derive(Debug)]
も裏で追加しています)
#![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);
}
pure
のシグネチャが結構微妙かも。pure
に用意されている型パラメータと Id
が保持する型パラメータが一致していなくて、ターボフィッシュが必要になってしまっている。ちょっと実装間違えたかな。
#![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);
}
やっぱ実装間違ってた(たぶん参考にした元記事も間違ってる)。
Pointed
の pure
には独自の型パラメータは必要ない。Functor
の持つ A
を使用したらいい。Functor
を下記のように直すだけで、
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 の呼び出しもスッキリさせられる。
#![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);
}
MonadError
を用意していくとこんな感じになるかな?ちょっと使うかわからないので型クラスだけ適当に置いておいて放置します🙏 for-yield のような制御構造を使用する際には必須になりそうなので一応用意。
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;
}
do notation についてはこのシリーズを真似させてもらおうかな。
ざっくりまとめると、
-
do!
マクロで do 記法を開始できる。 -
let a! = ...
でa <- ...
を再現できる。 -
return!(...)
でreturn ...
を再現できる。 - 内部は
ControlFlow
という enum で表現されていて、do!
マクロは実質ControlFlow
のシンタックスシュガーになっている。
つまりマクロを使ってなんとかする感じなのか。それはそうという気はする。
こんな感じのマクロを作れたら最高かなあ。
右辺がきちんと Monad を実装しているかを確認して、実装されていれば bind をして、bind 時の値を取り出す。それを2回繰り返して、yield 時には Functor を確認して map が実装されているかを確認し、実装されていれば yield の後ろの関数を実行するイメージ。
do! {
i1 <- Id::pure(1)
i2 <- Id::pure(2)
return i1 + i2
}
結局マクロは結構簡単に作れた。
アドベントカレンダーに記事を公開しました。