Haskellは楽しいことが分かったが、圏論のことを1ミリも知らないので数学上の定義とプログラムの対応がよくわからないままLYHGG読んでなんかわかったようなわかんねえような状態になっていた。
要は同じcontext同士で演算できてその結果もcontextですよって話で雑に納得しておいた。あと副作用を値として扱うというのはLYHGGに書いてある範囲で納得いった気にはなれた。
普通にRust書いててResultだのOptionだのにコンビネーター生えててそこでやってるのと変わらんって感じ。だからあいつらmonadicとか言われてたのか。なるほどな。
ということであえてRustでFunctorとかApplicative FunctorとMonadを自前してみようと息巻いてみたものの、Rustには型クラスみたいな概念がないと思ったのでどうやるのかよくわからないなあと思いながら手を付けていったら実力が足りず終わっていった。
準備
何はともあれ適当に恒等関数を作っておく。
fn id<T: Clone>(x: &T) -> T {
x.clone().to_owned()
}
それと先述した通りRustには型クラスって概念が基本的にはないはずだからうまいことhigher kindをなんとかしないといけない。ところで higher kindとは型コンストラクタを表すやつでこのようなもの。
Prelude Data.Functor> :k Functor
Functor :: (* -> *) -> Constraint
高階という名に違わず型を生み出すための型クラスのシグネチャになっていることがわかる(Constraintカインドは割愛)。
*
を型パラメーターとして関連型で持っておいて Constraint
にあたるものが higher kinded な trait とすればいいんだろう。
現時点で必要なのは (* -> *)
なので、そのような型コンストラクタを表現する必要がある。
極力自分で考えたいが、流石に全部自前は無理だと思うので適宜先人の知恵を参考にする。
trait HKT<B> {
type A;
type TB;
}
関連型を使って A -> B
という射を表せばいいんだろうから型パラメーターとしてBを定義しておけばいいのでこんな感じか。
TBはBをcontextでラップしなおした値として表現している。
Functor
Functorはcontextの中身の値を関数に適用してその結果を同じcontextに戻してして返すというやつ。
まずFunctor則はhackageから拝借すると
The Functor class is used for types that can be mapped over. Instances of Functor should satisfy the following laws:
fmap id == id
fmap (f . g) == fmap f . fmap g
ということなので恒等射と射の合成の保存を満たさないといけない。
カリー化するの面倒だし、とりあえずはメソッド形式にするとしてfmapはこんな感じだろうか。
trait Functor<B>: HKT<B> {
fn fmap<F: FnOnce(&Self::A) -> B>(&self, f: F) -> Self::TB;
}
実際にMaybeを実装してみるとして、HKTでtrait束縛してるからMaybeにも実装しないといけない。
#[cfg(test)]
mod functor_spec {
use super::{HKT, id, Functor};
#[derive(PartialEq, Eq, Debug, Clone)]
enum Maybe<T> {
Just(T),
Nothing,
}
impl<A, B> HKT<B> for Maybe<A> {
type A = A;
type TB = Maybe<B>;
}
impl<A, B> Functor<B> for Maybe<A> {
fn fmap<F: FnOnce(&Self::A) -> B>(&self, f: F) -> Self::TB {
match self {
Self::Just(x) => Maybe::Just(f(x)),
Self::Nothing => Maybe::Nothing,
}
}
}
#[test]
fn satisfied_id() {
let just_2 = Maybe::Just(2);
assert_eq!(just_2.fmap(id), id(&just_2));
let nothing: Maybe<usize> = Maybe::Nothing;
assert_eq!(nothing.fmap(id), id(¬hing));
}
#[test]
fn satisfied_composition() {
fn f(x: &usize) -> String {
format!("value: {}", x)
}
fn g(x: &usize) -> usize {
*x + 1
}
fn f_g(x: &usize) -> String {
f(&g(x))
}
let just_2 = Maybe::Just(2);
assert_eq!(
just_2.fmap(f_g),
just_2.fmap(g).fmap(f),
);
}
}
なんかできたような気がする。
Applicative Functor
自分には無理だった。
Functor の実装ができた感じがあるのでこのままApplicative Functorに挑戦しようと思ったが思わぬ型パズルに苦戦したところでむなしくなったのであきらめた。
Applicative Functorは (<*>) :: f (a -> b) -> f a -> f b
と定義されるように関数を値としてもつことができるFunctorの強化版であり、型コンストラクタには Functor である必要がある。
ここで <*>
にあたる操作をメソッドチェーンとして表現することになりそうな時点であきらめたくなってきているが、とりあえずできるところまでやってみよう。
知恵を参考にしつつこんな感じになった。
trait Applicative<B>: Functor<B> {
fn pure(v: B) -> Self::TB where Self: HKT<B, A=B>;
fn combine<F: FnOnce(&<Self as HKT<B>>::A) -> B>(&self, f: <Self as HKT<F>>::TB) -> <Self as HKT<B>>::TB
where
Self: HKT<F>
;
}
意図としてはApplicative Functorとしてラップしておく関数をHKTの型パラメーターに指定することでTBがApplicative Functorのcontextでラップされた関数であるように表現してる感じ。
でもなんかもういろいろと厳しさを感じる。
そもそも関連型のTBをってちゃんとコンパイラに伝えられるんだろうか。Maybeに実装してみよう。
impl<A, B> Applicative<B> for Maybe<A> {
fn pure(v: B) -> <Maybe<A> as HKT<B>>::TB {
Maybe::Just(v)
}
fn combine<F: FnOnce(&<Self as HKT<B>>::A) -> B>(&self, f: <Self as HKT<F>>::TB) -> Self::TB
where
Self: HKT<F>
{
match self {
Self::Just(x) => match f {
Maybe::Just(f_) => Maybe::Just(f_(x)),
Maybe::Nothing => Maybe::Nothing,
},
Self::Nothing => Maybe::Nothing,
}
}
}
コンパイルしてみると
error[E0308]: mismatched types
--> src/main.rs:53:17
|
25 | Nothing,
| ------- unit variant defined here
...
53 | Maybe::Nothing => Maybe::Nothing,
| ^^^^^^^^^^^^^^ expected associated type, found enum `Maybe`
|
= note: expected associated type `<Maybe<A> as HKT<F>>::TB`
found enum `Maybe<_>`
= help: consider constraining the associated type `<Maybe<A> as HKT<F>>::TB` to `Maybe<_>` or calling a method that returns `<Maybe<A> as HKT<F>>::TB`
= note: for more information, visit https://doc.rust-lang.org/book/ch19-03-advanced-traits.html
ああ……
Monad はさらに Applicative Functor の強化版なのでこの時点で無理なので以降実装はあきらめる。
数学的な厳密な定義とかはなんもわからんので抜きにしてMonadがなんなのかといえばものすごく実用上の視点で見れば、演算をおこなう >>=
のシグネチャを見てみると (>>=) :: forall a b. m a -> (a -> m b) -> m b
とある。
RustのResultやOptionのmapそのものではないか。
以上!
Haskellの定義上は fail
があるので全く同じではないんだけど、Applicative Functorと違ってbindされる関数はcontextを意識することを強制されずに値を取ることができるという点がありそれに着目してしまうと Haskellのdo式が便利ですねという身も蓋もない話に帰結しまうのであった。
結論
やりようはあるんだろうけど適材適所がある。
Rustも楽しいけど考え方が変わるHaskellも楽しくていい教材だ。