More than 1 year has passed since last update.

モナドを勉強しようとするとモナド則が立ちはだかります。しかしモナド則は意味だけでなく、それがモナドを勉強する上でどういう位置付けなのかも分かりにくいです。とりあえず棚上げしたまま分かる範囲のことから手を付けていましたが、ちょっと分かったような気がしたのでメモしておきます。

  • モナド則に関する現時点の理解ですが、正確さは保証できません。
  • これを読めばモナドが理解できるという類の説明(モナドチュートリアル)ではありません。
  • 試験的にモナド則を図示しました。コードを図に転写して、多少の解釈を加えたものです。もし分かりにくければ無視しても構いません。

この記事はHaskell 超入門シリーズの番外編です。

モナド則

  1. return x >>= f == f x
  2. m >>= return == m
  3. (m >>= f) >>= g == m >>= (\x -> f x >>= g)

※ 余談ですが、いきなりこういうのを見せられたときの感覚が、次のツイートでうまく表現されていると思いました。

何でないか

  • モナドを勉強するには、まずモナド則を理解しないといけないわけではありません
  • モナド則を理解すれば、モナドがすっきり分かるというものではありません

何であるか

  • モナド則は、モナドを作るときに守らないといけないルールです。
  • モナドを使う側の立場からは、コードを整理するのに役立ちそうです。

モナドの勉強

モナドをどう勉強するかについて、現段階で感じていることです。

  • モナドの勉強にあたって、モナドの作り方から入るのは難しいです。オブジェクト指向を勉強するときに、クラスの作り方から入るようなものです。(それは難しいよね、という意味合いです)
  • モナドが何かという抽象的な問いかけはしなくても、単なるプログラミングテクニックとして既存のモナドは使えそうです。オブジェクト指向で例えれば、デザインパターンに沿ってきれいな設計のクラスが作れなくても、既存のクラスは使えるようなものです。

※ もちろん最終的にはモナドが作れるに越したことはありませんが、ここで想定しているのは学習の初期段階です。

次のような見解もあります。

モナド・ミニマム

return>>=で操作できてモナド則を満たすものをモナドと呼びます。

モナドはよく箱だと言われます。中に値が入った箱です。

Monad.png

リストの実体もモナドで、まさに値が入った箱です(例:[1])。そのためモナド代表としてリストで説明します。

return

returnはモナドという箱に値を入れるための関数です。

return.png

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とは型が異なることが確認できました。

型表記を単純にするため、1Intであることを指定します。

Prelude> :t return (1 :: Int)
return (1 :: Int) :: Monad m => m Int

モナドの型を指定します。リスト型は[]です。

Prelude> return 1 :: [] Int
[1]

型を指定することで、returnによりリストモナドに1という値を入れることができました。複雑な書き方をしていますが、得られるものは[1]と同じです。

>>=

モナドを関数に渡すための演算子が>>=(バインドと読む)です。

モナド >>= 関数で以下の処理が行われます。(漏斗に例えています)

bind.png

  1. モナドから値が取り出されます。
  2. 取り出された値が関数に引数として渡されます。
  3. 関数では戻り値をモナドに入れて返します。

関数はモナドを返す必要があります。一番簡単な対応方法は計算結果を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#での実装例を示します。オブジェクト指向でモナドを模倣すると冗長になります。

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です。モナドの>>=も同じような目的で使うことができると考えても、当たらずと雖も遠からずではないでしょうか。

※ 実際には>>=は関数呼び出しの前後に処理を挟むことができるため、メソッドチェインよりも強力です。今回の範囲を超えるため詳細は省略しますが、興味があれば次の記事を参照してください。

モナド則を確認

モナド則はreturn>>=の動きに関するルールです。

  • 各モナド則は式を == で区切り、左辺と右辺が等しいかを見ます。
    • 等しくなるものがモナドだ、というルールです。
    • 等しくならないものは作れますが、それはモナドではありません。
  • 確認例では結果の分かりやすさを考慮してリストを使います。
    • 理解の便宜として一例を示しただけです。証明ではありません。

1. return x >>= f == f x

returnでモナドに値xを入れてからすぐ>>=xを取り出して関数fに渡すのは、値xを関数fに直接渡すのと等しいです。左辺は出し入れしているだけです。

1.png

Prelude> let x = 1
Prelude> let f x = [x + 1]
Prelude> return x >>= f
[2]
Prelude> f x
[2]

当たり前に思えます。

2. m >>= return == m

モナドmから>>=で値を取り出してreturnでまたモナドに入れれば、それは元のmと等しいです。左辺は出し入れしているだけです。

2.png

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により、関数testreturnは省略できます。

test x = inc $ x * 2

コーディングスタイルの是非はともかく、このような変形を保証しているのがモナド則2です。

3. (m >>= f) >>= g == m >>= (\x -> f x >>= g)

結合法則のようなものです。掛け算のようなものをイメージします。

  • (m * f) * g == m * (f * g)

mf >>= g == m >>= fgのように見立てます。どちらもmfgの順に渡されます。処理される順番が同じなので、fgがどのような関数であっても結果は同じです。

3.png

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 >>= (\x -> f x >>= g)のラムダ式の範囲を明示するとm >>= (\x -> (f x >>= g))となります。
    • m >>= (f >>= g)m >>= ((\x -> f x) >>= g)ではありません。これらは型エラーになります。
    • モナド用の関数合成演算子>=>を使えばm >>= (f >=> g)となります。
    • 通常の関数合成で書けばm >>= ((>>= g) . f)となります。パズルみたいな見た目です。

関数合成

ラムダ式の部分を関数合成で書き換える過程を示します。

  1. m >>= (\x -> f x >>= g)
  2. $を水増し: m >>= (\x -> (>>= g) $ f x)
  3. 式を括弧で囲む: m >>= (\x -> ((>>= g) $ f x))
  4. $.: m >>= (\x -> ((>>= g) . f) x)
  5. ポイントフリースタイル: m >>= ((>>= g) . f)

詳細は次の記事を参照してください。

まとめ

モナドを実装する人が適切にreturn>>=を実装すれば、モナド則に沿った「当たり前な動き」が実現されます。そうならないように実装することは可能ですが、それはモナドではありません。それを判定するのがモナド則だということのようです。

モナドを使うだけの人にとっては「コードを整理するためのヒント」になりそうですが、わざわざルールとして勉強しなくても、モナドの動きが分かっていれば同等の変形は自然と気付きそうです。

参考

以下の記事を参考にしました。

モナド解説の金字塔です。(内容が重いためつまみ食いしかしていませんが・・・)

モナド則3の右辺の式変形は、次の掲示板を参考にしました。

別の描き方でモナドを図示して説明した記事です。

それとはまた別の描き方による説明です。

ここで使用されている String diagram についての記事です。

関連記事

この記事で使用したモナド則の図は次の記事が初出です。以前は続編という位置付けでした。

図の要素は次の記事で導入しています。

F#に応用した記事があります。