關於函數式編程(Functional Programming)的學習筆記Ⅰ

關於函數式編程(Functional Programming)的學習筆記Ⅰ

原文鏈接:http://www.cnblogs.com/TivonStone/archive/2010/10/08/1845569.html

0.要點
1.背景知識
2.函數式編程(FP)
3.FP的特性


1.背景知識


  在C# 3.0中加入的新特性中,最大頭的一個就是Lambda表達式,微軟利用這個Lambda表達式結合其它一些特性演繹了很多東西,Lambda表達式和擴展方法一起帶來了Linq to Objects,從Lambda表達式推演出來的表達式樹又是Linq to SQL等的基礎。而另一門語言F#也在悄悄崛起,Lambda表達式和F#都歸屬於函數編程範疇。

  我們肯定都知道馮.諾依曼,因爲他是計算機之父,因爲他的馮.諾依曼體系結構。我們肯定都知道阿蘭.圖靈,人工智能之父,著名的圖靈獎堪稱計算機業的諾貝爾。在這個時代還存在另外一個傳奇人物:阿隆左·丘奇(Alonzo Church),知道這個人的請舉手,反正我是很晚才知道。1936年,圖靈的一篇論文:論數字計算在決斷難題中的應用,在這篇論文中,提出了圖靈機的設想,這也是今天計算機的基礎,因爲他不僅僅提出了設想,還在理論上證明了這種機器是可以製造出來的,不過在這之前,還有一個Lambda演算的理論,該理論奠定後來的Lisp語言(值得注意的是,Lisp是我們擁有的第二個計算機高級語言,僅次於Forton之後)和函數編程的基礎。


2.什麼是函數式編程(Functional Programming)

  函數式編程利用數學上的函數來避免狀態(這個可是圖靈機的基礎,圖靈機利用狀態去判定下一步工作,而函數式編程卻不需要狀態)和可變的數據(今天,如果沒有變量,我真的不知道我該怎麼編寫代碼)。看起來非常的奇妙,但這一切都是Lambda演算賜予的。

      Wiki百科的定義是:是種編程範型,它將電腦運算視爲函數的計算。函數編程語言最重要的基礎是 λ 演算(lambda calculus)。而且λ演算的函數可以接受函數當作輸入(引數)和輸出(傳出值)。命令式編程相比,函數式編程強調函數的計算比指令的運行重要。過程化編程相比,函數式編程裏,函數的計算可隨時調用。

     函數是函數式編程的基本單位,函數幾乎被用於一切,包括最簡單的計算,甚至變量都由計算取代。在函數式編程中,變量只是表達式的別名(這樣我們就不必把所有東西打在一行裏)。變量是不能更改的,所有變量只能被 賦值一次。用 Java 的術語來說,這意味着所有單一變量都被聲明爲 final(或 C++ 的 const)。在函數式編程中沒有非 final 的變量。



final int i = 5;
final int j = i + 3;

 

  因爲函數式編程中所有變量都是 final 的,所以可以提出這樣兩個有趣的表述:沒有必要總是寫出關鍵字 final,沒有必要把變量再稱爲變量。那麼現在我們對Java作出兩個修改:在我們的函數式 Java 中所有變量默認都是 final的,我們將變量(variable)稱爲符號(symbol)。

      就此你也許會質疑,用我們新創造的語言還能寫出有些複雜度的程序嗎?如果每個符號都是不可變更(non-mutalbe)的,那麼就無法改變任何狀態!其實事實並非完全如此。在阿隆左研究其 lambda 演算時,他並不想將某個狀態維護一段時間以期未來對其進行修改。他關注的是對數據的操作(也通常被稱爲”演算體 caculating stuff”)。既然已被證明lambda演算與圖靈機等價,它可以完成所有命令式編程語言能夠完成的任務。那麼,我們怎麼才能做到呢?

     答案是函數式程序能保存狀態,只是它並非通過變量而是使用函數來保存狀態。狀態保存在函數的參數中,保存在堆棧上。如果你要保存某個狀態一段時間並時不時地對其進行一些修改,可以寫個遞歸函數。舉個例子,我們寫個函數來翻轉 Java 的字符串。記住,我們聲明的每個變量默認都是 final 的。


String reverse(String arg) {
    if(arg.length == 0) {
        return arg;
    }
    else {
    return reverse(arg.substring(1, arg.length)) + arg.substring(0,1);
    }
}

3.函數式編程的特性

  • 閉包和高階函數

       函數本身是first class對象,閉包是起函數作用並可以像對象一樣操作的。 
       高階函數是可以接受一個函數爲參數,並可以返回一個函數。 

  • 延遲計算(lazy evaluation)
  • 遞歸的計算機制
  • 引用透明:      同樣的輸入返回同樣的結果,與上下文無關。   
  • 沒有副作用:      賦值後不能更改,既成爲final


單元測試

因爲函數式編程的每一個符號都是 final 的,沒有函數產生過副作用。因爲從未在某個地方修改過值,也沒有函數修改過在其作用域之外的量並被其他函數使用(如類成員或全局變量)。這意味着函數求值的結果只是其返回值,而惟一影響其返回值的就是函數的參數。

     這是單元測試的理想環境。對被測試程序中的每個函數,你只需在意其參數,而不必考慮函數調用順序,不用謹慎地設置外部狀態。所有要做的就是傳遞代表了邊際情況的參數。如果程序中的每個函數都通過了單元測試,你就對這個軟件的質量有了相當的自信。而命令式編程就不能這樣樂觀了,在 Java 或 C++ 中只檢查函數的返回值還不夠——我們還必須驗證這個函數可能修改了的外部狀態。


調試

因爲函數式編程的每一個符號都是 final 的,沒有函數產生過副作用。因爲從未在某個地方修改過值,也沒有函數修改過在其作用域之外的量並被其他函數使用(如類成員或全局變量)。這意味着函數求值的結果只是其返回值,而惟一影響其返回值的就是函數的參數。

     這是單元測試的理想環境。對被測試程序中的每個函數,你只需在意其參數,而不必考慮函數調用順序,不用謹慎地設置外部狀態。所有要做的就是傳遞代表了邊際情況的參數。如果程序中的每個函數都通過了單元測試,你就對這個軟件的質量有了相當的自信。而命令式編程就不能這樣樂觀了,在 Java 或 C++ 中只檢查函數的返回值還不夠——我們還必須驗證這個函數可能修改了的外部狀態。


並行

  函數式程序無需任何修改即可並行執行。不用擔心死鎖和臨界區,因爲你從未用鎖!函數式程序裏沒有任何數據被同一線程修改兩次,更不用說兩個不同的線程了。這意味着可以不假思索地簡單增加線程而不會引發折磨着並行應用程序的傳統問題。

      事實既然如此,爲什麼並不是所有人都在需要高度並行作業的應用中採用函數式程序?嗯,他們正在這樣做。愛立信公司設計了一種叫作 Erlang 的函數式語言並將它使用在需要極高抗錯性和可擴展性的電信交換機上。還有很多人也發現了 Erlang 的優勢並開始使用它。我們談論的是電信通信控制系統,這與設計華爾街的典型系統相比對可靠性和可升級性要求高了得多。實際上,Erlang 系統並不可靠和易擴展,Java 纔是。Erlang 系統只是堅如磐石。

      關於並行的故事還沒有就此停止,即使你的程序本身就是單線程的,那麼函數式程序的編譯器仍然可以優化它使其運行於多個CPU上。請看下面這段代碼:

String s1 = somewhatLongOperation1();
String s2 = somewhatLongOperation2();
String s3 = concatenate(s1, s2);

  在函數編程語言中,編譯器會分析代碼,辨認出潛在耗時的創建字符串s1和s2的函數,然後並行地運行它們。這在命令式語言中是不可能的,因爲在那 裏,每個函數都有可能修改了函數作用域以外的狀態並且其後續的函數又會依賴這些修改。在函數式語言裏,自動分析函數並找出適合並行執行的候選函數簡單的像 自動進行的函數內聯化!在這個意義上,函數式風格的程序是“不會過時的技術(future proof)”(即使不喜歡用行業術語,但這回要破例一次)。硬件廠商已經無法讓CPU運行得更快了,於是他們增加了處理器核心的速度並因並行而獲得了四 倍的速度提升。當然他們也順便忘記提及我們的多花的錢只是用在瞭解決平行問題的軟件上了。一小部分的命令式軟件和 100% 的函數式軟件都可以直接並行運行於這些機器上。 


代碼熱部署

過去要在 Windows上安裝更新,重啓計算機是難免的,而且還不只一次,即使是安裝了一個新版的媒體播放器。Windows XP 大大改進了這一狀態,但仍不理想(我今天工作時運行了Windows Update,現在一個煩人的圖標總是顯示在托盤裏除非我重啓一次機器)。Unix系統一直以來以更好的模式運行,安裝更新時只需停止系統相關的組件,而 不是整個操作系統。即使如此,對一個大規模的服務器應用這還是不能令人滿意的。電信系統必須100%的時間運行,因爲如果在系統更新時緊急撥號失效,就可 能造成生命的損失。華爾街的公司也沒有理由必須在週末停止服務以安裝更新。

       理想的情況是完全不停止系統任何組件來更新相關的代碼。在命令式的世界裏這是不可能的。考慮運行時上載一個Java類並重載一個新的定義,那麼所有 這個類的實例都將不可用,因爲它們被保存的狀態丟失了。我們可以着手寫些繁瑣的版本控制代碼來解決這個問題,然後將這個類的所有實例序列化,再銷燬這些實例,繼而用這個類新的定義來重新創建這些實例,然後載入先前被序列化的數據並希望載入代碼可以恰當地將這些數據移植到新的實例。在此之上,每次更新都要重新手動編寫這些用來移植的代碼,而且要相當謹慎地防止破壞對象間的相互關係。理論簡單,但實踐可不容易。

       對函數式的程序,所有的狀態即傳遞給函數的參數都被保存在了堆棧上,這使的熱部署輕而易舉!實際上,所有我們需要做的就是對工作中的代碼和新版本的 代碼做一個差異比較,然後部署新代碼。其他的工作將由一個語言工具自動完成!如果你認爲這是個科幻故事,請再思考一下。多年來 Erlang工程師一直更新着他們的運轉着的系統,而無需中斷它。


機器輔助的推理和優化

函數式語言的一個有趣的屬性就是他們可以用數學方式推理。因爲一種函數式語言只是一個形式系統的實現,所有在紙上完成的運算都可以應用於用這種語言 書寫的程序。編譯器可以用數學理論將轉換一段代碼轉換爲等價的但卻更高效的代碼。多年來關係數據庫一直在進行着這類優化。沒有理由不能把這一技術應 用到常規軟件上。

       另外,還能使用這些技術來證明部分程序的正確,甚至可能創建工具來分析代碼併爲單元測試自動生成邊界用例!對穩固的系統這種功能沒有價值,但如果你 要設計心房脈衝產生器 (pace maker)或空中交通控制系統,這種工具就不可或缺。如果你編寫的應用程序不是產業的核心任務,這類工具也是你強於競爭對手的殺手鐗。


高階函數

函數式語言提供了不同的抽象工具它會使你忘記你曾經習慣於修改變量。高階函數就是這樣一種工具。

     創建函數的方式和 C 中相似:


 
 int add(int i, int j) 
            {return i + j;}

  現在擴展我們的 Java 編譯器使其支持這種記法。當我們輸入上述代碼後編譯器會把它轉換成下面的Java代碼(別忘了,所有東西都是 final 的):

class add_function_t {
          int add(int i, int j) {
                return i + j;
          }
      }
 
      add_function_t add = new add_function_t();


這裏的符號 add 並不是一個函數。這是一個有一個成員函數的很小的類。我們現在可以把 add 作爲函數參數放入我們的代碼中。還可以把它賦給另一個符號。

     我們在運行時創建的 add_function_t 的實例如果不再被使用就將會被垃圾回收掉。這些使得函數成爲第一級的對象無異於整數或字符串。(作爲參數)操作函數的函數被稱爲高階函數。別讓這個術語嚇 着你,這和 Java 的 class 操作其它 class(把它們作爲參數)沒有什麼區別。我們本可以把它們稱爲“高階類”但沒有人注意到這個,因爲 Java 背後沒有一個強大的學術社區。

      如果你發現在那個函數裏一些邏輯動作根據情況有變,就把他提取成高階函數。高階函數只是開始!



惰性求值

惰性(或延遲)求值這一技術可能會變得非常有趣一旦我們採納了函數式哲學。在討論並行時已經見過下面的代碼片斷:

String s1 = somewhatLongOperation1();
String s2 = somewhatLongOperation2();
String s3 = concatenate(s1, s2);

在一個命令式語言中求值順序是確定的,因爲每個函數都有可能會變更或依賴於外部狀態,所以就必須有序的執行這些函數:首先是somewhatLongOperation1,然後 somewhatLongOperation2,最後 concatenate,在函數式語言裏就不盡然了。

       前面提到只要確保沒有函數修改或依賴於全局變量,somewhatLongOperation1 和 somewhatLongOperation2 可以被並行執行。但是如果我們不想同時運行這兩個函數,還有必要保證有序的執行他們呢?答案是不。我們只在其他函數依賴於s1和s2時才需要執行這兩個函 數。我們甚至在concatenate調用之前都不必執行他們——可以把他們的求值延遲到concatenate函數內實際用到他們的位置。如果用一個帶 有條件分支的函數替換concatenate並且只用了兩個參數中的一個,另一個參數就永遠沒有必要被求值。在 Haskell 語言中,不確保一切都(完全)按順序執行,因爲 Haskell 只在必要時纔會對其求值。

       惰性求值優點衆多,但缺點也不少。我不在這裏列出,有興趣的朋友可以在這裏看。



原文鏈接:http://www.cnblogs.com/TivonStone/archive/2010/10/08/1845569.html


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章