FAIC モナド、パート6
2018-01-19 16:35:18
この連載では、モナド・パターンの本当のルールを前回やっと導くことができた。C#におけるパターンは、モナドは M<T>
ジェネリック型で、
T
型の力を「増幅する」というものだ。T
型の値から M<T>
を生成する方法がいつでも存在する。それは次のようなヘルパーメソッドが存在することで表した:
static M<T> CreateSimpleM<T>(T t)
また、A
型のどんな値でも受け取って M<R>
型の値を返す関数があれば、その関数を M<A>
型のインスタンスに適用して、やはり M<R>
型の値を返す方法が存在する。それは次のようなヘルパーメソッドが存在することで表した:
static M<R> ApplySpecialFunction<A, R>(
M<A> wrapped,
Func<A, M<R>> function)
これで終わり?まだだ、まだ終わらんよ。モナド・パターンの正しい実装を得るためには、これら2つのヘルパーメソッドにいくつかの制約を追加して、ヘルパーメソッドが行儀よくふるまうことを保証しないといけない。具体的にはこうだ: 1つ目の生成ヘルパー関数は値を「ラップ」するものとみなせるし、2つ目の適用ヘルパー関数は値を「開封」する方法を知っている; であれば、 値をラップして開封する操作を行ったときに、元の値が保持されていることを要求 してもいいだろう。
そのことを念頭に置くと、ApplySpecialFunction
が2番目の引数として A
を M<R>
に変える関数を受け取っているが、
それは任意の A
と任意の R
であるということに気づく。一方、CreateSimpleM
は T
を M<T>
に変える関数なのだから、つまり ApplySpecialFunction
の引数となりえる! 以下のようなコードがあったとしよう: 1
static Nullable<T> CreateSimpleNullable<T>(T t)
{
return new Nullable<T>(t);
}
static Nullable<R> ApplySpecialFunction<A, R>(
Nullable<A> nullable,
Func<A, Nullable<R>> function)
{
return nullable.HasValue ?
function(nullable.Value) :
new Nullable<R>();
}
そして、CreateSimpleNullable
のシグネチャを見れば、ApplySpecialFunction
の第2引数として渡しても問題ないことが分かっているので:
Nullable<int> original = Whatever();
Nullable<int> result =
ApplySpecialFunction(original, CreateSimpleNullable);
というコードは想定通りの動作をする。もし original
がヌルだったら、ヌルが戻ってくる。もし original
が12みたいに値を持っていたら、値は開封されて MakeSimpleNullable
に渡されるので、ラップされた12が戻ってくる!このルールは次の通り:
「値の単純なラッパーを作る」関数をモナド値に適用したら、元のモナド値が得られなければならない。
そしてこの場合に必要なものは実際には 値の同一性 だ。気を付けてほしいのは、 参照の同一性 は要求していないこと。モナド型がたまたま参照型だったとしてもね。OnDemand<T>
モナドも見てみよう:
static OnDemand<T> CreateSimpleOnDemand<T>(T t)
{
return () => t;
}
static OnDemand<R> ApplySpecialFunction<A, R>(
OnDemand<A> onDemand,
Func<A, OnDemand<R>> function)
{
return ()=>function(onDemand())();
}
ここで以下のようなコードを書けば、
OnDemand<int> original = () => DateTime.Now.Seconds;
OnDemand<int> result =
ApplySpecialFunction(original, CreateSimpleOnDemand);
original
と result
の参照は一致しないはずだ。でも original
と result
は同じことをする: これらを呼び出したら、現在の秒数が得られる。result
の方は不要な輪っかを潜り抜けないといけないのが残念だけれど、でも結果は同じだ。
モナド・パターンの実装の中には、2つのインスタンスが 参照として 等しいことを簡単に保証できるものもあるだろう。それはそれでいいことだ。でも、実際に必要なのは、あるモナドに単純な生成関数を適用したときに、元のインスタンスと結果のインスタンスが 意味的に 等しいということだけだ。
次の制約は、「値の単純なラッパー」が、実際に値の単純なラッパーとしてふるまうということだ。でもそれってどう表現すればいい?これは例の2つのヘルパーメソッドがあれば簡単なことだ。実例を見てみよう。SafeLog
メソッドを思い出してほしい:
static Nullable<double> SafeLog(int value) { ... }
ここで以下のようなコードを書けば:
int original = 123;
Nullable<double> result1 = SafeLog(original);
Nullable<int> nullable = CreateSimpleNullable(original);
Nullable<double> result2 = ApplySpecialFunction(nullable, SafeLog);
result1
と result2
は 同じヌル許容 double
になると思わない?もし Nullable<int>
が int
の単なるラッパーだとすれば、ラッパーの方に関数を適用しても、元の整数値に関数を適用するのと同じことになるはずだ。このルールを一般化すると、あるモナド化関数をある値に適用した結果と、「モナドでラップされた」同じ値に適用した結果は、同じ2にならなければならないと言える。
さて、またここでまとめよう。モナド・パターンのルールは、モナド型 M<T>
には次のメソッドと論理的に等価な操作が提供されるということだ:
static M<T> CreateSimpleM<T>(T t) { ... }
static M<R> ApplySpecialFunction<A, R>(
M<A> monad, Func<A, M<R>> function) {...}
これらのメソッドは次のような制約に従わなければならない:
ApplySpecialFunction(someMonadValue, CreateSimpleM)
が someMonadValue
と論理的に等しい値を返すことと、
ApplySpecialFunction(CreateSimpleM(someValue), someFunction)
が
someFunction(someValue)
と論理的に等しい値を返すことだ。
もうこれで完璧でしょ?そうでしょ?
…… いいや。 モナド・パターンには これでもまだ ルールが1つ足りていない。でも信じてほしい、これが本当の最後だ。 次回のFAIC では、プログラミングの本質について、もっと言えばすべての問題解決の本質について議論する。そしてモナド・パターンがそこに当てはまることがわかるだろう。その過程で、最後のルールが導かれる。