モナドを勉強しようとするとモナド則が立ちはだかります。しかしモナド則は意味だけでなく、それがモナドを勉強する上でどういう位置付けなのかも分かりにくいです。とりあえず棚上げしたまま分かる範囲のことから手を付けていましたが、ちょっと分かったような気がしたのでメモしておきます。
- モナド則に関する現時点の理解ですが、正確さは保証できません。
- これを読めばモナドが理解できるという類の説明(モナドチュートリアル)ではありません。
- 試験的にモナド則を図示しました。コードを図に転写して、多少の解釈を加えたものです。もし分かりにくければ無視しても構いません。
この記事はHaskell 超入門シリーズの番外編です。
モナド則
モナド則は return
と >>=
の動きに関するルールです。
-
return x >>= f
==f x
-
m >>= return
==m
-
(m >>= f) >>= g
==m >>= (\x -> f x >>= g)
各モナド則は式を == で区切り、左辺と右辺が等しいかを見ます。等しくなるものがモナドだ、というルールです。等しくならないものは作れますが、それはモナドではありません。
解釈を書いておきます。(詳細は後述)
-
>>=
の左側のreturn
が消せることを要請します。 -
>>=
の右側のreturn
が消せることを要請します。 - あらかじめ関数が合成できることを要請します。
※ 余談ですが、いきなりこういうのを見せられたときの感覚が、次のツイートでうまく表現されていると思いました。
最初に骸骨を見せて「これからこれに肉付けをして美女にします」というのがいまの大学の数学科のカリキュラムみたいなもんね。最初から美女のイメージ持ってる人は楽しいのかもしれないけど、そうでない人はいつまでも骸骨と一緒にワルツを踊ってなきゃいけない。
— ゼルプスト殿下 (@tenapyon) 2014年12月1日
何でないか
- モナドを勉強するには、まずモナド則を理解しないといけないわけではありません。
- モナド則を理解すれば、モナドがすっきり分かるというものではありません。
何であるか
- モナド則は、モナドを作るときに守らないといけないルールです。
- モナドを使う側の立場からは、コードを整理するのに役立ちそうです。
モナドの勉強
モナドをどう勉強するかについて、現段階で感じていることです。
- モナドの勉強にあたって、モナドの作り方から入るのは難しいです。オブジェクト指向を勉強するときに、クラスの作り方から入るようなものです。(それは難しいよね、という意味合いです)
- モナドが何かという抽象的な問いかけはしなくても、単なるプログラミングテクニックとして既存のモナドは使えそうです。オブジェクト指向で例えれば、デザインパターンに沿ってきれいな設計のクラスが作れなくても、既存のクラスは使えるようなものです。
※ もちろん最終的にはモナドが作れるに越したことはありませんが、ここで想定しているのは学習の初期段階です。
次のような見解もあります。
モナド則とか、理解しなくてもいいですって。分かれば当たり前のことだし、分からなくてもプログラムは書けるし。教えちゃうから、分からないってなっちゃう。知らないといけないのは、モナドを自分で作るようになってから。
— 山本和彦 (@kazu_yamamoto) 2013, 11月 28
モナド・ミニマム
return
と>>=
で操作できてモナド則を満たすものをモナドと呼びます。
モナドはよく箱だと言われます。中に値が入った箱です。
リストの実体もモナドで、まさに値が入った箱です(例:[1]
)。そのためモナド代表としてリストで説明します。
return
return
はモナドという箱に値を入れるための関数です。
ghciで確認します。
Prelude> return 1
1
これだけでは単なる1
と区別が付かないので、:t
で型を調べます。
Prelude> :t 1
1 :: Num a => a
Prelude> :t return 1
return 1 :: (Monad m, Num a) => m a
単なる1
と、モナドに入れられた1
とは型が異なることが確認できました。
※ 一番下の行の =>
の右側の m a
に注目してください。
型表記を単純にするため、1
がInt
であることを指定します。
Prelude> :t return (1 :: Int)
return (1 :: Int) :: Monad m => m Int
※ m a
が m Int
に変化したことに注目してください。
モナドの型を指定します。リスト型は[]
です。
Prelude> return 1 :: [] Int
[1]
※ m Int
の m
を []
に置き換えて [] Int
となります。[Int]
とも書けます。
型を指定することで、return
によりリストモナドに1
という値を入れることができました。複雑な書き方をしていますが、得られるものは[1]
と同じです。
>>=
モナドを関数に渡すための演算子が>>=
(バインドと読む)です。
モナド >>= 関数
で以下の処理が行われます。(漏斗に例えています)
- モナドから値が取り出されます。
- 取り出された値が関数に引数として渡されます。
- 関数では戻り値をモナドに入れて返します。
関数はモナドを返す必要があります。一番簡単な対応方法は計算結果をreturn
でモナドに入れることです。
Prelude> let f x = return $ x + 1
動作を確認します。
Prelude> [1] >>= f
[2]
モナドから1
が取り出されて関数f
に渡され、関数の戻り値が表示されました。
連続
わざわざ>>=
などというものを用意して、しかもモナドから値の出し入れなどという複雑なことをしているのは、それを連続させたいからのようです。
Prelude> [1] >>= f >>= f >>= f >>= f
[5]
これで何が嬉しいのかは、具体的に意味のあるものを作ってみないと実感しにくいです。しかし簡単なサンプルは思い付かないので、別の言語で似た例を示します。
メソッドチェイン
>>=
を連続させる使い方は、オブジェクト指向言語におけるメソッドチェインに似ています。
x >>= f >>= f >>= f >>= f
x.f().f().f().f()
C#での実装例を示します。オブジェクト指向でモナドを模倣すると冗長になります。
using System;
class Test
{
private readonly int x;
private Test(int x)
{
this.x = x;
}
public static Test @return(int x)
{
return new Test(x);
}
public Test f()
{
return @return(x + 1);
}
public override string ToString()
{
return "[" + x + "]";
}
static void Main()
{
Console.WriteLine(@return(1).f().f().f().f());
}
}
[5]
C#でメソッドチェインが多用されるのはLinqやRxです。モナドの>>=
も同じような目的で使うことができると考えても、当たらずと雖も遠からずではないでしょうか。
※ 実際には>>=
は関数呼び出しの前後に処理を挟むことができるため、メソッドチェインよりも強力です。今回の範囲を超えるため詳細は省略しますが、興味があれば次の記事を参照してください。
- @7shi: Haskell Maybeモナド 超入門 2015.1.22
モナド則を確認
結果の分かりやすさを考慮してリストで確認します。
※ 理解の便宜として一例を示しただけです。証明ではありません。
1. return x >>= f
== f x
>>=
の左側の return
が消せることを要請します。
return
でモナドに値x
を入れてからすぐ>>=
でx
を取り出して関数f
に渡すのは、値x
を関数f
に直接渡すのと等しいです。左辺は出し入れしているだけです。
Prelude> let x = 1
Prelude> let f x = [x + 1]
Prelude> return x >>= f
[2]
Prelude> f x
[2]
当たり前に思えます。
2. m >>= return
== m
>>=
の右側の return
が消せることを要請します。
モナドm
から>>=
で値を取り出してreturn
でまたモナドに入れれば、それは元のm
と等しいです。左辺は出し入れしているだけです。
Prelude> let m = [1]
Prelude> m >>= return
[1]
これもまた当たり前に思えます。
return
C言語などの感覚でコードを見ると、ブロックの最後にreturn
があるとしっくり来ます。
inc x = return $ x + 1
test x = do
y <- inc $ x * 2
return y
main = do
print =<< test 3
モナド則2により、関数test
のreturn
は省略できます。
test x = inc $ x * 2
コーディングスタイルの是非はともかく、このような変形を保証しているのがモナド則2です。
3. (m >>= f) >>= g
== m >>= (\x -> f x >>= g)
あらかじめ関数が合成できることを要請します。
右辺のラムダ式を関数化して fg x = f x >>= g
とおけば fg
は f
と g
の関数合成に相当します。
※ f
と g
の間にモナドから値を取り出す操作 >>=
が必要なため、単純な関数合成 g . f
では型エラーとなります。(>>= g) . f
とする必要があります。f >=> g
とも書けます。(詳細は後述)
fg
によって書き換えると (m >>= f) >>= g
== m >>= fg
となります。両辺とも m
が f
→ g
の順に渡されます。処理される順番が同じなので、f
や g
がどのような関数であっても結果は同じです。
Prelude> let m = [1]
Prelude> let f x = return $ x + 1
Prelude> let g x = return $ x * 2
Prelude> (m >>= f) >>= g
[4]
Prelude> m >>= (\x -> f x >>= g)
[4]
見かけはややこしいですが、理屈は当たり前です。
※ モナド則1と2を満たして3だけを満たさないものは、意図的に作ろうとしてもなかなか難しいです。詳細はコメントを参照してください。
注意
-
>>=
は左結合のため、左辺(m >>= f) >>= g
の括弧は外せます:m >>= f >>= g
-
m >>= (f >>= g)
は型エラーになります。
-
- 右辺
m >>= (\x -> f x >>= g)
のラムダ式の範囲を明示するとm >>= (\x -> (f x >>= g))
です。-
m >>= ((\x -> f x) >>= g)
ではありません。これは型エラーになります。\x -> f x
をポイントフリー化すればf
となるため、前述の型エラーになるm >>= (f >>= g)
と一致します。(やはりダメだということです)
-
- モナドに適用する関数同士は専用の演算子
>=>
で合成できます:fg = f >=> g
-
>=>
によって右辺m >>= (\x -> f x >>= g)
を書き換えればm >>= (f >=> g)
となります。これはあらかじめ関数を合成しているのが分かりやすい表記です。 -
m >>= (f >>= g)
とは書けませんが、m >>= (f >=> g)
とは書けるという見方もできます。
-
まとめ
モナドを実装する人が適切にreturn
と>>=
を実装すれば、モナド則に沿った「当たり前な動き」が実現されます。そうならないように実装することは可能ですが、それはモナドではありません。それを判定するのがモナド則だということのようです。
モナドを使うだけの人にとっては「コードを整理するためのヒント」になりそうですが、わざわざルールとして勉強しなくても、モナドの動きが分かっていれば同等の変形は自然と気付きそうです。
補足
補足情報です。よく分からなければ無視しても構いません。
モナド則3の書き換え
モナド則3の書き換えを並べます。
-
(m >>= f) >>= g
==m >>= (\x -> f x >>= g)
(ラムダ式) -
(m >>= f) >>= g
==m >>= fg where fg x = f x >>= g
(関数化) -
(m >>= f) >>= g
==m >>= (f >=> g)
(専用の演算子) -
(m >>= f) >>= g
==m >>= ((>>= g) . f)
(関数合成)
演算子の使い分けにさえ慣れれば、3で考えるのがお勧めです。特に結合法則が読み取りやすいです。
>=>
については次の記事が参考になります。
- @CyLomw: (>>=) を (>=>) に書き換える 2014.12.30
関数合成
モナド則3を普通の関数合成で書けば m >>= ((>>= g) . f)
となります。パズルみたいな見た目です。意味が取りにくい書き方ですが、参考のため説明します。
考え方としては f x >>= g
を f x
と (>>= g)
に分割して、引数 x
を取り除き、f
と (>>= g)
を関数合成したのが (>>= g) . f
です。
※ f
と g
の間にモナドから値を取り出す操作 >>=
が入ることに注意します。(>>= g)
は g
がモナドを受け取るように変換された関数だと解釈します。(この変換を関数の持ち上げと表現します)
書き換える過程を示します。
-
f x >>= g
をf x
とセクション(>>= g)
に分割して、前者を後者の引数とします。-
f x >>= g
→(>>= g) (f x)
-
-
(>>= g)
とf
を合成して、引数x
を括り出します。-
(>>= g) (f x)
→((>>= g) . f) x
-
- ラムダ式の引数をそのまま適用している形になるため、引数を取り除きます。(ポイントフリースタイル)
-
\x -> ((>>= g) . f) x
→(>>= g) . f
-
詳細は次の記事を参照してください。
- @7shi: Haskell - 関数合成を機械的に扱う試み - Qiita 2015.2.9
モノイド対象
モナドは「自己関手の圏におけるモノイド対象」だと言われます。モナド則そのままでは分かりにくいですが、>=>
で書き換えればモノイドの構造が現れます。
モナド則を再掲します。説明の都合上、モナド則3の関数名を変更しました。
-
return x >>= f
==f x
-
m >>= return
==m
-
(m >>= g) >>= h
==m >>= (\x -> g x >>= h)
モナドは何らかの関数から生成されたと考えて m = f x
で書き換えます。
-
return x >>= f
==f x
-
f x >>= return
==f x
-
(f x >>= g) >>= h
==f x >>= (\x -> g x >>= h)
あちこちにモナド則3のラムダ式の g x >>= h
と同じパターンが現れていることに注目します。それらを >=>
で書き換えます: g x >>= h
→ (g >=> h) x
-
(return >=> f) x
==f x
-
(f >=> return) x
==f x
-
(f >=> g) x >>= h
==f x >>= (g >=> h)
(f >=> g)
や (g >=> h)
は合成された1つの関数と見なせるため、3の両辺は更に書き換えられます。
-
((f >=> g) >=> h) x
==(f >=> (g >=> h)) x
引数 x
を取り除けば、モナド則は >=>
によって関数を合成する規則に書き換わります。
-
return >=> f
==f
-
f >=> return
==f
-
(f >=> g) >=> h
==f >=> (g >=> h)
>=>
を掛け算 *
のようなものだと考えれば、return
は 単位元 1
に相当します。
- 左単位元
1 * f
==f
- 右単位元
f * 1
==f
-
結合法則
(f * g) * h
==f * (g * h)
単位元を持ち結合法則を満たすものはモノイドです。
詳細は次の記事を参照してください。
- @tezca686: [Haskell]結局、モナド則は何を要求しているのか 2017.09.20
- @tezca686: [Haskell] 爆速でモナドを理解する 2018.10.15
行列との比較
$\vec v$ をベクトル、$A,B$ を $\vec v$ に作用する行列とします。
行列とベクトルの積が =<<
に対応します。
- $A\vec v$ :
A =<< v
行列同士の積が <=<
に対応します。
- $AB$ :
A <=< B
結合法則より $AB\vec v=(AB)\vec v=A(B\vec v)$
- $(AB)\vec v$ :
(A <=< B) =<< v
- $A(B\vec v)$ :
A =<< (B =<< v)
演算子を逆向きにすればモナド則3そのものです。
- $A(B\vec v) = (AB)\vec v$
-
A =<< (B =<< v)
==(A <=< B) =<< v
-
(v >>= B) >>= A
==v >>= (B >=> A)
行列を関数だと捉える見方については、以下の記事を参照してください。
参考
以下の記事を参考にしました。
- @kazu_yamamoto: モナド則 - あどけない話 2008.2.22
- @kazu_yamamoto: モナド則再び - あどけない話 2008.9.27
- @kazu_yamamoto: モナド則三度 - あどけない話 2008.12.3
モナド解説の金字塔です。(内容が重いためつまみ食いしかしていませんが・・・)
簡潔なモナドの説明です。「持ち上げ」の感覚が分かれば色々と見えて来ます。
- @tnagao7: モナド - ウォークスルー Haskell 2018.7.21
モナド則3の右辺の式変形は、次の掲示板を参考にしました。
- 純粋関数型言語Concurent Clean: 212-214 2005.12.15
別の描き方でモナドを図示して説明した記事です。
- @suin: 箱で考えるFunctor、ApplicativeそしてMonad 2013.11.25
それとはまた別の描き方による説明です。
From Adjunctions to Monads http://t.co/hbqfAHzLdN string diagramの方がまだ絵としては分かりやすい感じする
— みょん (@myuon_myon) 2014, 12月 12
- @myuon_myon: 「モナドは単なる自己関手の圏におけるモノイド対象だよ。何か問題でも?」 2014.12.30
ここで使用されている String diagram についての記事です。
- @okomok: String diagram のすすめ - PS 2015.12.20
関連記事
この記事で使用したモナド則の図は次の記事が初出です。以前は続編という位置付けでした。
- モナド則の絵を描いてみた 2014.12.12
図の要素は次の記事で導入しています。
- Haskell アクション 超入門 2014.12.13(改訂)
- Haskell IOモナド 超入門 2014.12.12(改訂)
F#に応用した記事があります。
- コンピュテーション式でモナドを作ってみる 2016.07.03
この記事で使用したのと似たような箱で型を考える記事です。
- 共変戻り値と反変引数 2016.11.27