さんぽみち

なにか思いついたときとかに気まぐれに更新されます。

C# 定数関数(遅延評価)のキャッシュ

はじめに

 遅延評価に定数関数を用いることが結構ある毎日です。
 定数関数が副作用を含んでいない時に何度も評価するのは意味がなく無駄に感じます。
 そこで、こんな悩みを解決する方法を今日思いついたので紹介したいと思います。

何も考えない時の実装

 評価を遅延させたいとき、よくこんな風に書きます。

using System;

public class MainApp
{
  public static void Main()
  {
    var add_1 = AddLazy(2, 3);
    var add_1_2 = AddLazy(add_1, add_1);
    var add_1_2_2 = AddLazy(add_1_2, add_1_2);
    Console.WriteLine("Lazy test");
    Console.WriteLine(add_1_2_2());
  }
  public static Func<int> AddLazy(int a, int b)
  {
    return () => {
      Console.WriteLine("calc: add num");             // テスト用
      return a + b;
    };
  }
  ///<summary>二つの定数関数の和を返す定数関数</summary>
  public static Func<int> AddLazy(Func<int> a, Func<int> b)
  {
    return () => {
      Console.WriteLine("calc: add const function");  // テスト用
      return a() + b();
    };
  }
}

実行結果

Lazy test
calc: add const function
calc: add const function
calc: add num
calc: add num
calc: add const function
calc: add num
calc: add num
20
続行するには何かキーを押してください . . .

 意図通り、値を使用する直前に値を評価しています。
 しかしこれ、a_1の計算を4回行い、a_1_2の計算を2回も行っています。
 このように、関数の結合を繰り返していくと最悪で結合を行った階層を n とすると 2n 回計算することになります。
 計算結果は同じなのにこんなに計算しなおすのはばからしいですね。

キャッシュ機能付きの遅延評価

 そこでこんなメソッドをつくってやって通してやります。

using System;

public class MainApp
{
  public static void Main()
  {
    var add_1 = ToCashe(AddLazy(2, 3));
    var add_1_2 = ToCashe(AddLazy(add_1, add_1));
    var add_1_2_2 = ToCashe(AddLazy(add_1_2, add_1_2));
    Console.WriteLine("Lazy test");
    Console.WriteLine(add_1_2_2());
  }
  public static Func<int> AddLazy(int a, int b)
  {
    return () => {
      Console.WriteLine("calc: add num");             // テスト用
      return a + b;
    };
  }
  ///<summary>二つの定数関数の和を返す定数関数</summary>
  public static Func<int> AddLazy(Func<int> a, Func<int> b)
  {
    return () => {
      Console.WriteLine("calc: add const function");  // テスト用
      return a() + b();
    };
  }
  
  public static Func<T> ToCashe<T>(Func<T> f)
  {
    Func<T> f_ = null;
    f_ = () => {
      var cashe = f();         // 渡された定数関数を評価
      f = null;                // fへの参照が切れる為、不要になればGCが回収してくれる
      f_ = () => cashe;        // 計算結果を直接返すデリゲートに置き換える
      return cashe;            // 1回目実行時の結果を返す
    };
    Func<T> ret = () => f_();  // f_自体を直接返すとデリゲートの置き換えができなくなる為
                               // 1段階かます
    return ret;
  }
}

実行結果

Lazy test
calc: add const function
calc: add const function
calc: add num
20
続行するには何かキーを押してください . . .

 それぞれ1回ずつしか呼ばれていませんね!
 クロージャにキャッシュさせることで管理を楽にしつつデリゲートそのものを2回目から変えてやることで分岐を排除しています。
 さらに、 a_1_2_2 を戻り値として返したとき、一度だけ実行してやると a_1_2 と a_1 はGCの対象となり、不要なキャッシュは勝手に消してくれます。
 やっていいのなら、拡張メソッドにしておくとなお便利になります。
 ぜひやってみてね!