FAIC モナド、パート9
2018-01-19 17:05:18
前回のFAICでは、モナド・パターンで使われている標準的な用語について話した。単純な生成メソッドは伝統的に「unit」と呼ばれているし、関数適用メソッドは伝統的に「bind」と呼ばれている。また、モナドパターンの実装で一番よく使われているサブ・パターンを2つ取り上げた。1つ目は、1つの値に対してある状態を関連付けることで、2つ目は、将来実行される処理単位のシーケンスを表現するワークフローを生成することだ。今回は1つ目の種類のモナドについて詳細を見ていこうと思う。
これまで、ヌル許容モナドについてはいろいろ話してきた。このモナドは根本的には元の値と1つのBoolean値を関連付けるものだ。もしBoolean値が真なら、元の値はいつも通りにふるまう。そうでない場合は、値が「存在しない」状態がシステム内を伝播していく。1この例についてはこれ以上くどくど説明しない。その代りに、ヌル許容モナドの変種を2つ見てほしい:
struct Tainted<T>
{
public T Value { get; private set; }
public bool IsTainted { get; private set; }
private Tainted(T value, bool isTainted) : this()
{
this.Value = value;
this.IsTainted = isTainted;
}
public static Tainted<T> MakeTainted(T value)
{
return new Tainted<T>(value, true);
}
public static Tainted<T> MakeClean(T value)
{
return new Tainted<T>(value, false);
}
public static Tainted<R> Bind<A, R>(
Tainted<A> tainted, Func<A, Tainted<R>> function)
{
Tainted<R> result = function(tainted.Value);
if (tainted.IsTainted && !result.IsTainted)
return new Tainted<R>(result.Value, true);
else
return result;
}
}
(今回から、バインド関数を「ApplySpecialFunction」じゃなくて「Bind」としているのに注意)
このモナドの意味論はわりと明白だと思う。ある値に「汚れ(taint)」を関連付けたことで、「汚れた」値を引数として受け取る関数はすべて自動的に汚れた値を返す。言い換えると、このモナドに適用する関数がきれいな値を返すためには、元の値がきれいであり、その上で関数がきれいな値を生みだすことが必須になる。このモナドで文字列を「増幅」すれば、文字列がクロスサイト・スクリプティング攻撃のチェック済みかどうかを判断できる。文字列は、きれいであると証明されるまでは汚れているとみなされ、汚れた文字列に対する操作はかならず別の汚れた文字列を生成する。
この例はとても単純化されている。ここで保持するべき「セキュリティ」状態はもっとずっと複雑になりえるんじゃないかと思うかもしれない。1つの単純なBooleanで汚れを表現するのではなく、データがどのクライアントから来ているかを決めるIDになるかもしれない。そこはどうにでもなる。
ヌル許容モナドの2つ目のバリエーションは以下だ:
struct NoThrow<T>
{
private T value;
public T Value
{
get
{
if (this.Exception != null)
throw this.Exception;
else
return this.Value;
}
}
public Exception Exception { get; private set; }
private NoThrow(T value, Exception exception) : this()
{
this.value = value;
this.Exception = exception;
}
public NoThrow(T value) : this(value, null) {}
public static NoThrow<R> Bind<A, R>(
NoThrow<A> noThrow, Func<A, NoThrow<R>> function)
{
if (noThrow.Exception != null)
return new NoThrow<R>(default(R), noThrow.Exception);
R newValue;
try
{
newValue = function(noThrow.Value));
}
catch(Exception ex)
{
return new NoThrow<R>(default(R), ex);
}
return new NoThrow<R>(newValue, null);
}
}
こっちも意味論はかなり明白だ。これはヌル許容モナドの強化版で、モナドを生成する操作が例外を投げた場合は値は「ヌル」になる。非同期モナド Task<T>
を使ったことがある人にとってはこのモナドは親しみやすいだろう。なぜなら想像通り同じようなことをするからだ: 非同期タスクが例外を投げる場合はその例外はタスクの中に格納され、タスクの値が読み出されるときに再スローされる。2
モナドを使えば、どんな種類の状態であっても値に関連付けることができる。例えば、ある値がどのようにプログラムを流れたかを表すログをデバッグのために集めておきたいと思うかもしれない:
struct Logged<T>
{
public T Value { get; private set; }
public string Log { get; private set; }
public Logged(T value, string log) : this()
{
this.value = value;
this.log = log;
}
public Logged(T value) : this(value, null) {}
public Logged<T> AddLog(string newLog)
{
return new Logged(this.value, this.log + newLog);
}
public static Logged<R> Bind<A, R>(
Logged<A> logged, Func<A, Logged<R>> function)
{
Logged result = function(logged.Value);
return new Logged(result.Value, logged.Log + result.Log);
}
}
こういった例は「データを値に関連付ける」モナドとして考えられる中でも比較的単純なものでしかないが、概念は理解できたものと思う。次回のFAIC では、クエリ式の文法について詳しく見ていく。クエリ式はモナドのバインディングと強い関連があるとわかるはずだ。