10.1.2.1 C# 和 F# 中可重用的記憶化

來源:互聯網
上載者:User

標籤:

10.1.2.1 C# 和 F# 中可重用的記憶化

 

如果看一下清單 10.3 中建立 add 值的代碼,可以發現,它並不真正知道加法,只是使用了 addSimple 函數,因此,也可以處理其他任何函數。為了使代碼更通用,我們可以把這個函數改成參數。

我們要寫一個函數(C# 中叫方法),參數為函數,返回這個函數的記憶化版本。參數值是做實際工作的函數,返回的函數增加了緩衝功能。清單 10.4 是C# 版本的代碼。

 

清單 10.4 泛型記憶化方法 (C#)

Func<T, R> Memoize<T,R>(Func<T, R> func) {   [1]

  var cache = new Dictionary<T,R>();   <-- 由閉包捕獲緩衝

  return arg => { 

    R val; 

    if(cache.TryGetValue(arg, out val)) return val;   <-- 返回緩衝值

    else { 

      val =func(arg);      | 計算值,

     cache.Add(arg, val);  | 加到緩衝中

      returnval;          |

    } }; 

}

 

代碼類似於清單 10.3 中的專用加法函數;另外,我們首先建立一個緩衝,然後,返回在閉包中捕捉緩衝的匿名函式。這樣,對於每個返回的函數,將只有一個緩衝,這正是我們想要的。

方法簽名[1]表明,參數為函數 Func<T, R>,返回相同類型的函數。這樣,不改變函數的結構,只是把它封裝成另一個實現緩衝的函數。簽名是泛型的,所以,它可以使用任何接受一個參數的函數;用元組就能突破這個限制。下面的代碼實現了兩個數字相加的記憶化函數的 C# 版本:

 

var addMem = Memoize((Tuple<int, int>arg) => { 

  Console.Write("adding {0} +{1}; ", arg.Item1, arg.Item2); 

  return arg.Item1 + arg.Item2; });

 

Console.Write("{0}; ",addMem(Tuple.Create(19, 23)));   |[1]

Console.Write("{0}; ",addMem(Tuple.Create(19, 23)));  |

 

Console.Write("{0}; ", addMem(Tuple.Create(18,24)));    [2]

 

運行這段代碼,會發現第二段代碼只輸出“adding 19+23”一次,第三塊輸出“adding 18 + 24”。這就是說,前面的加法只執行一次,因為,緩衝比較兩個元組值,當它們的元素都相等時,就找到了一個匹配。這不會處理元組的第一個實現,因為,它沒有任何相等的值實現;在 .NET 4.0 中的元群組類型,以及本章原始碼都是重寫(override)了Equals 方法,進行組件值的比較,這稱為結構比較(structural comparison),我們會在第十一章詳細介紹。要使 Memoize 方法能夠處理有多個參數的函數,還有一種選擇,重載 Func<T1, T2, R>、Func<T1, T2, T3, R>,等等。在緩衝中,仍然把元組作為鍵,但對於方法的使用者來說,它是隱藏的。

[

overload和override的區別:

override(重寫)

1、方法名、參數、返回值相同。

2、子類方法不能縮小父類方法的存取權限。

3、子類方法不能拋出比父類方法更多的異常(但子類方法可以不拋出異常)。

4、存在於父類和子類之間。

5、方法被定義為final不能被重寫。

overload(重載)

1、參數類型、個數、順序至少有一個不相同。 

2、不能重載只有返回值不同的方法名。

3、存在於父類和子類、同類中。

]

在 F# 中,實現的泛型同樣代碼更加容易。我們將取清單 10.3 中寫的加法代碼,把計算的函數作為記憶化函數的參數。可以在清單 10.5 中看到 F# 版本。

 

清單10.5 泛型記憶化方法(F# Interactive)

> let memoize(f) = 

     let cache = newDictionary<_, _>()   <-- 初始化由閉包捕獲的緩衝

     (fun x –> 

       matchcache.TryGetValue(x) with 

       |true, v –> v 

       | _-> let v = f(x) 

         cache.Add(x, v) 

         v);; 

val memoize : (‘a -> ‘b) -> (‘a ->‘b)    [1]

 

在 F# 版本中,可以推斷出類型簽名[1],所以,我們不必要手工使函數成為泛型。F# 編譯器使用泛型化(generalization)就能做到,推斷出的簽名相當於 C# 代碼中顯式簽名。

這一次,我們將使用更有意義的例子來示範記憶化的有效。我們會回到全世界最喜歡的遞迴樣本:階乘函數。清單 10.6 試圖對這個函數進行記憶化(memoize),但並不完全按照計劃......

 

清單10.6 用實現記憶化的遞迴函式的困難 (F# Interactive)

> let rec factorial(x) =     [1]

     printf"factorial(%d); " x 

     if (x <= 0)then 1 else x * factorial(x - 1);;  [2]

val factorial : int –> int

 

> let factorialMem = memoizefactorial   [3]

val factorial : (int -> int)

> factorialMem(2);; 

factorial(2); factorial(1);factorial(0);   <-- 第一次計算 2!

val it : int = 1

 

> factorialMem(2);; 

val it : int = 1    <-- 使用緩衝的值

 

> factorialMem(3);; 

factorial(3); factorial(2); factorial(1);factorial(0)   [4] 為什麼這個 2! 會重新計算

val it : int = 2

 

乍一看,代碼似乎是正確的。首先把階乘計算實現為簡單的遞迴函式[1],然後,使用memoize(記憶化)函數,建立函數的最佳化版本[3]。我們在後面進行測試,運行相同的調用兩次,看上去正常。第一次調用後,結果被緩衝,因此,可以重複使用。

最後一個調用[4]就不正確了,或更確切地說,沒有按照我們所希望的。問題出在記憶化只覆蓋了第一次調用,即 factorialMem(3)。factorial 函數在隨後的遞迴計算期間,直接調用了原始的函數,沒有調用記憶化的版本。要解決這個問題,需要把進行遞迴調用的這一行代碼[2],改成使用記憶化的版本(factorialMem)。這個函數在後面的代碼中聲明,所以,可以使用 let rec ... and ... 文法來聲明兩個相互遞迴的函數。

還有一個簡單的方法,就是使用 匿名函式,只把記憶化的版本公開為可重用的函數。清單 10.7 只用幾行代碼實現這個函數。

 

清單10.7改正後可記憶化的 factorial 函數 (F# Interactive)

> let rec factorial = memoize(fun x-> 

     printfn"Calculating factorial(%d)" x 

     if (x <= 0)then 1 else x * factorial(x - 1));;    [1]

warning FS0040: This and other recursivereferences to the        |

object(s) being defined will be checked forinitializationsoundness  | [2]

at runtime through the use of a delayedreference...             |

 

val factorial : (int -> int)

 

> factorial(2);; 

factorial(2); factorial(1);factorial(0);   <-- 先計算幾個值

val it : int = 2

 

> factorial(4);; 

factorial(4); factorial(3);   <-- 只計算沒有的值

val it : int = 24

 

在這個例子中的 factorial 表示值,它不是文法意義上定義為有參數的函數,相反,它就是值(恰好是函數類型),由 memoize 函數返回的。這就是說,我們並沒有聲明遞迴的函數,而是聲明遞迴的值。我們在第八章建立決策樹時,用 let rec聲明遞迴值,但是,我們只用它來以更自然的方式寫節點,在代碼中並沒有任何的遞迴調用。

這一次,我們建立了真正遞迴的值,因為,值 factorial 在聲明它自己的時候就用到了。遞迴值的困難在於,如果我們不小心,寫出引用某個值的代碼,可能會在這個值的初始化期間,是無效的操作。下面是不正確初始化的例子:

 

let initialize(f) = f() 

let rec num = initialize (fun _ -> num +1)

 

在這裡,對值 num 的引用出現在 匿名函式的內部,在初始化期間被調用,當 initialize 函數被調用時,匿名函式被調用。如果運行這個代碼,在 num 被聲明的地方,會出現執行階段錯誤。使用遞迴函式時,函數總是在執行遞迴調用的時候,才定義;代碼可能永遠保持迴圈,但是,這是不同的問題。

我們聲明的 factorial,對 factorial 值的引用出現在 匿名函式中,並不在初始化期間調用,所以,是有效聲明。而F# 編譯器在編譯時間無法區分這兩種情況,所以,會發出警告[2],並增加了運行時檢查。不要過於害怕這個!只要確保包含引用的 匿名函式不在初始化過程中進行計算,就行了。

由於 factorial 聲明進行遞迴調用時,使用記憶化版本,現在可以在任意計算步驟中,讀緩衝的值。例如,當我們計算 2 的階乘後,再計算 4 的階乘,只需要計算剩下的兩個值。

 

注意

 

到目前為止,我們已經看到了函數編程中的兩種最佳化方法。使用尾遞迴,要吧避免棧溢出,寫出更好的遞迴函式;記憶化,能夠最佳化任何無副作用的函數。

這兩種方法都非常完美地適合反覆式開發法的風格,被認為是 F# 編程的重要方面。我們可以從簡單的實現開始,往往是一個函數,可能是遞迴的,無副作用。在隨後的過程中,對代碼中需要最佳化的地方進行確認。正如我們前面看到的,演變代碼的結構,添加物件導向方面,都很容易,最佳化所需要的改變也相當簡單。迭代過程能夠在複雜的領域花很小的代價,效益顯著。

 

到目前為止,我們已經看到寫高效函數的通用技巧;還有一種資料結構,本身適合於非常具體的最佳化:集合類型。在下一節,我們要討論函數式列表,以及以函數方式使用 .NET 數組。

 

10.1.2.1 C# 和 F# 中可重用的記憶化

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.