matarillo.com

The best days are ahead of us.

FAIC モナド、パート4

2018-01-19 09:33:18

原文

これまでに、モナド・パターンに沿った型があれば、「元の」型の値から「ラップされた」値をいつでも生み出せることを見てきた。また、5種類のモナド的な型それぞれにおける、ラップされた整数値に1を足す方法と、それによって作り出された新しい値が望みの「増幅」を保持する方法を見てきた – ヌル許容や遅延初期化など。今回は3、4歩前進して1、整数値に1を足す以外の操作に対するパターンを一般化できるか調べよう。

そうはいってもまだしばらくは整数値の話を続けよう。高階プログラミングは難易度がちょっと高いのでゆっくりアプローチしたいのだ。整数値に関する操作を Func<int, int> – すなわち、1つの整数値を受け取って1つの整数値を返すデリゲート – で表現する。前回の5種類の AddOne メソッドを一般化して、どのような操作でも表現できる Func<int, int> を受け取るようにすることができる。

static Nullable<int> ApplyFunction(
  Nullable<int> nullable, 
  Func<int, int> function)
{
  if (nullable.HasValue)
  {
    int unwrapped = nullable.Value;
    int result = function(unwrapped);
    return new Nullable<int>(result);
  }
  else
    return new Nullable<int>();
}

これで、 AddOne メソッドは単なる特殊な場合の1つとなった:

static Nullable<int> AddOne(Nullable<int> nullable)
{
  return ApplyFunction(nullable, (int x) => x + 1);
}

この単純な変形を他の AddOne メソッドに適用して、5つとも ApplyFunction メソッドにすることができることがわかるはずだ。さらに一般化できるだろうか?もちろん。整数値でなければいけない理由などない!整数は全部 T 型に置き換えてしまえる。

static Nullable<T> ApplyFunction<T>(
  Nullable<T> nullable, 
  Func<T, T> function)
{
  if (nullable.HasValue)
  {
    T unwrapped = nullable.Value;
    T result = function(unwrapped);
    return new Nullable<T>(result);
  }
  else
    return new Nullable<T>();
}

まだ続きがある: intをdoubleにする関数の場合は?たとえば、intを2で割った答えをdoubleで受け取りたい場合とか。当然、intからdoubleを得る操作があるならば、その操作を「増幅された」intに適用して、「増幅された」doubleを得ることができなければならない。そうすると、型引数が2つ必要だ: 入力側の元の型と、出力側の元の型だ:

static Nullable<R> ApplyFunction<A, R>(
  Nullable<A> nullable, 
  Func<A, R> function)
{
  if (nullable.HasValue)
  {
    A unwrapped = nullable.Value;
    R result = function(unwrapped);
    return new Nullable<R>(result);
  }
  else
    return new Nullable<R>();
}

そして、モナド・パターンのほかの例でも同じように、 ApplyFunction メソッドは同じことができるように変更できる:

static Lazy<R> ApplyFunction<A, R>(
  Lazy<A> lazy, 
  Func<A, R> function)
{
  return new Lazy(()=> 
  {
    A unwrapped = lazy.Value;
    R result = function(unwrapped);
    return result;
  });  
}

以下同様; 前回わざと冗長に書いておいた残りの例は簡単な練習問題だ。

モナド・パターンの1つめの要件は、「元の型の値を増幅された型の値に変える簡単な方法がなくてはならない」だ。そして前の方で言ったように、いつでも元の型の値に戻す方法があるべきだというのはもっともらしく思えるけれど、真実はもっと微妙なものだ。ここで示されたことは、実際に値を戻す必要があるのは、 元の値に対する任意の関数を、任意の増幅された値に適用できることを保証しないといけない場合だけ ということだ。

基本的には、ここで得られたものは AR にする関数を、 M<A>M<R> にする関数に変える方法のひとつであり、その場合、関数の挙動が保存されるだけでなく、モナド的な型による「増幅」も保存されている。これはクソ便利だ!

ということは、モナド・パターンの要件の2つ目はこれなのだろうか: 次のようなシグネチャのメソッドを書くことができること?

static M<R> ApplyFunction<A, R>(
  M<A> amplified, 
  Func<A, R> function)

これで正しいだろうか?

いや、正しくない。でも すごく近い ! 次回のFAIC は、 ApplyFunction のシグネチャに小さいけどすごく重要な変更をほどこし、モナド・パターンの正しい2つ目の要件を得る


インデックスへ戻る


  1. 3月4日だけにね(キリッ)だっておwwwwwwwwwwバカスwwwwwww ↩︎