🎃

Functional Programming in C# まとめ 11章

2024/03/11に公開

10章のまとめは、こちら

11. Lazy computaions, continuatios, and the beauty of monadic compositon

この章では以下の内容を説明する。

  • 遅延実行
  • Tryを使った例外ハンドリング
  • monadicな関数合成
  • 継続を使って破滅のピラミッドから逃れる

11.1. The virtue of laziness

以下のコードは、引数に与えたどちらか一方の式の結果しか使わないが、両方が評価されてしまう。

var rand = new Random();

T Pick<T>(T l, T r) => rand.NextDboule() < 0.5 ? l : r;

Pick(1 + 2, 3 + 4);
// => 3 or 7

以下のコードの場合、実際に必要となる方の式しか評価されない

var rand = new Random();

T Pick<T>(Func<T> l, Func<T> r) => (rand.NextDboule() < 0.5 ? l : r)();

Pick(() => 1 + 2, () => 3 + 4);
// => 3 or 7

与えた式が重い計算を伴うものだった時に、遅延実行は有効な選択肢となる。

Func<T>はfunctorであることがわかっている。Mapの実装は以下:

public static Func<R> Map<T, R>
    (this Func<T> f, Func<T, R> g)
    => () => g(f());

単なる関数合成になっていることに注意。

Func<T>はmonadでもある:

public static Func<R> Bind<T, R>
    (this Func<T> f, Func<T, Func<R>> g)
    => () => g(f())();

この後、関数はなんであれ、monadということで、議論が進んでいくのだけれど、引数を取る関数についても議論してほしいなと思いました。1引数関数は、x => () => f(x)で引数を取らない関数を返す関数に簡単に変換することはできるのですが。

11.2. Exception handling with Try

例外を返すような関数を実行するときに以下のようなデリゲート型を考える

public delegate Exceptional<T> Try<T>();

Func<T>を特殊化したものですが、戻り値がExceptional<T>という別のeffectを持っているので、少し工夫が必要という議論です。これを通じて、monadicな合成を具体的に説明しています。

11.2.4. Monadic compositon: what does it mean?

計算の"monadicな合成"は、複雑な用語に思えるかもしれないが、本当はそんなことはない。

f: A -> M<B>
g: B -> M<C>

を合成したい場合、そのままではできないが、M<B>からBを取りだして、合成できるようにする。これがmonadicな合成だ。

言い換えると、monadicな合成は関数合成よりも一般的に関数を組み合わせる方法であり、関数の合成方法を決定するロジックをBind関数に含んでいる。

11.3. Creating a middleware pipeline for DB access

"継続"(continuation)に関する議論。著者は、Middlewareと名付けているようです。

public static T Time<T>(ILogger log, string op, Func<T> f)
{
    var sw = new Stopwatch();
    sw.Start();
    T t = f();
    sw.Stop();
    log.LogDebug($"{op} took {sw.ElapsedMilliseconds}ms");
    return t;
}

こういうような関数や、usingブロック使うような関数に、ある処理(関数)を渡して実行させたい。しかし、1度の実行で、こういう関数を複数適用したい場合、ネストが深くなり、読みづらくなってしまう。

var b = Time(logger, "一段目",
                () => Time(logger, "二段目", 
                    () => Time(logger, "三段目", () => 2 + 3)));

これを解消するためにBindを定義したい。上のような関数は以下のように抽象化できる

(T -> R) -> R

これをデリゲートで表現する

public delegate dynamic Middleware<T>(Func<T, dynamic> cont);

※Rでなく、dynamicなのは、C#が型引数の部分適用を受け付けてくれないためです。

Middleware<T>はmonadだ。これは直感に反する気がする。Tに関するmonadは、Tや複数のTを含んでいるものだからである。しかし、関数T -> Rを与えると、Rを返す関数なので、Middleware<T>の中にはTがすでに含まれている。

Bind、Mapは以下:

public static Middleware<R> Bind<T, R>
    (this Middleware<T> mw, Func<T, Middleware<R>> f)
    => cont
    => mw(t => f(t)(cont));

public static Middleware<R> Map<T, R>
    (this Middleware<T> mw, Func<T, R> f)
    => cont
    => mw(t => cont(f(t)));

dynamicだと、型安全でないので、型安全にするために、拡張メソッドを追加する。

public static T Run<T>(this Middleware<T> mw)
    =>(T)mw(t => t)

これらを使って、以下のように書くことができる。

Func<string, Middleware<Unit>> Time = op => f => Time(logger, op, f.ToNullary());

from _ in Time("一段目")
from _ in Time("二段目")
from _ in Time("三段目")
select 2 + 3;

ToNullaryは、1引数関数をゼロ引数関数へ変換する拡張メソッド※Timeは引数のない関数を取るので。

これで、追加・削除・順番の入れ替えが非常に簡単になった。

Discussion