よく使用される便利な暗記手法の例として、コンパイラーがコードを変換するものを見てみましょう。 明確にするために、F#自体でコードを記述し、C#で逆コンパイルします。
したがって、元の例:
let memoize f = let cache = System.Collections.Generic.Dictionary() fun x -> if cache.ContainsKey(x) then cache.[x] else let res = fx cache.[x] <- res res let rec memoizedFactorial = let factorial x = match x with | a when a = 0I -> 1I | x -> x * memoizedFactorial (x - 1I) memoize factorial let now = System.DateTime.Now let arguments = [10000I .. 10100I] let factorials = arguments |> List.map memoizedFactorial let timeDiff = System.DateTime.Now - now printfn "Time: %A" timeDiff
いくつかの説明:
- 10,000から10,100までの101階乗を考慮します。もっと長くするために。
- 計算の実行時間に注意してください。
- F#には組み込みの暗記がないため、自家製の暗記を使用します。 関数を引数として受け取り、キャッシュを初期化し、キャッシュ内の要素の存在を確認するために使用されるラムダ式を作成し、要素が存在しない場合は実際の関数を呼び出し、要素をキャッシュに追加します。
- 明確にするために、私は別個の記憶関数
memoizedFactorial
を作成しました。 この効果は、引数なしでmemoizedFactorial
関数を定義し、その結果、後で引数を渡す別の関数を返すという事実に基づいています。 したがって、memoizedFactorial
はオブジェクトとして単一のインスタンスに存在し、キャッシュは複数の関数呼び出しで同じであることがわかります。
さて、記念が何につながったのか見てみましょう。
記憶なしのプログラム実行時間は28.97秒で、記憶ありでは0.89秒です。 ことわざにあるように、効果は明らかです。
これがすべてコンパイルするものを調べます。 C#プログラマーにとって最も興味深い点は、もちろん
memoizedFactorial
関数を呼び出す瞬間です。 ローカル関数キャッシュが再作成されないのはなぜですか?
見てみましょう。
main
機能:
public static void main@() { memoizedFactorial@11-1 = LazyExtensions.Create<FSharpFunc<BigInteger, BigInteger>>(new Program.clo@11-1()); memoizedFactorial@11 = Program.memoizedFactorial@11.get_Value(); now@18 = DateTime.Now; arguments@20 = SeqModule.ToList<BigInteger>(new Program.arguments@20(0, new BigInteger())); factorials@21 = ListModule.Map<BigInteger, BigInteger>(Program.memoizedFactorial, Program.arguments); timeDiff@23 = (TimeSpan) (DateTime.Now - Program.now); fp@1 = new PrintfFormat<FSharpFunc<TimeSpan, Unit>, TextWriter, Unit, Unit, TimeSpan>("Time: %A"); PrintfModule.PrintFormatLineToTextWriter<FSharpFunc<TimeSpan, Unit>>(Console.Out, Program.fp@1).Invoke(Program.timeDiff); }
うーん...
memoizedFactorial
関数
memoizedFactorial
別のオブジェクトになりました。 さらに、変数はまったくありません。これらはすべて、クラス
internal static class $Program
フィールドになりました。 一般に、特定のクラスまで、コードは明確でなければなりません。
memoizedFactorial
入ります。
まず、
$Program
クラスの2つのフィールドがあります。
internal static FSharpFunc<BigInteger, BigInteger> memoizedFactorial@11; internal static Lazy<FSharpFunc<BigInteger, BigInteger>> memoizedFactorial@11-1;
2番目は、クロージャーが渡される標準クラスのオブジェクトによって初期化されることがわかります。 最初のフィールドは興味深いことを何もせず、それ自体を返します(
get_Value
メソッド)。
したがって、クロージャは次のようになります。
internal class clo@11-1 : FSharpFunc<Unit, FSharpFunc<BigInteger, BigInteger>> { // Methods internal clo@11-1(); public override FSharpFunc<BigInteger, BigInteger> Invoke(Unit arg00@); }
これは興味深いメソッドを持つクラスであることを理解しています。
public override FSharpFunc<BigInteger, BigInteger> Invoke(Unit arg00@) { return Program.memoizedFactorial@11-1(); }
うん 彼らはすでにいた場所に来たようです。 しかし、これはそうではありません。 これは、
memoizedFactorial@11-1
関数を呼び出した結果を返します。 最後に、この関数を見てみましょう:
internal static FSharpFunc<BigInteger, BigInteger> memoizedFactorial@11-1() { return memoize<BigInteger, BigInteger>(new clo@13()); }
こっち! 最後に、
memoize
関数に
memoize
。 そして別の閉鎖へ。 クロージャは同じクラスの継承クラスなので、すぐにInvokeメソッドを見てみましょう。
public override BigInteger Invoke(BigInteger x) { if (LanguagePrimitives.HashCompare.GenericEqualityIntrinsic<BigInteger>(x, BigInteger.Zero)) { return BigInteger.One; } return (x * Program.memoizedFactorial@11.get_Value().Invoke(x - BigInteger.One)); }
ここに階乗です! 値を直接計算する関数を見つけました。 F#コードで書いたように、メモリ化されたバージョンが再帰に使用されることに注意してください。 テンプレートとの比較は、通常の
if
に変換されました。 また、途中で、2つの
BigInteger
ハッシュを通じて比較されることに気付きました。
一歩戻って、
memoize
関数を見てみましょう。
public static FSharpFunc<a, b> memoize<a, b>(FSharpFunc<a, b> f) { return new memoize@3<a, b>(f, new Dictionary<a, b>()); }
ここから、楽しみが始まりました。 作業の結果として、オブジェクトは、クロージャー(最初の引数)と辞書(2番目の引数)が渡されるコンストラクターパラメーターに返されます。 これで、辞書が毎回再作成されない理由が明らかになりました。
私たちは深く行きます:
internal class memoize@3<a, b> : FSharpFunc<a, b> { // Fields public Dictionary<a, b> cache; public FSharpFunc<a, b> f; // Methods internal memoize@3(FSharpFunc<a, b> f, Dictionary<a, b> cache); public override b Invoke(ax); }
クラスフィールドは、キャッシュがプログラム全体のキャッシュである理由を再度示しています。
Invoke
関数を見てみましょう。
public override b Invoke(ax) { if (this.cache.ContainsKey(x)) { return this.cache[x]; } b res = this.f.Invoke(x); this.cache[x] = res; return res; }
そして、ここに私たちの記念機能があります! キーは引数を介して渡されます。 オブジェクトのフィールドを介してすでに知っているクロージャー
f
。 その秘密は解明されています。
したがって、F#には魔法はありません。 まあ、絶対に。 これらのすべてのカリー
memoizedFactorial
(および
memoizedFactorial
関数
memoizedFactorial
そのままです)、ファーストクラスの市民のような関数は、実際には単に自動生成されたクラスとオブジェクトです。 私の記事が、誰かがF#がどのように機能するかをよりよく理解するのに役立つことを願っています。