編程范式(PRogramming paradigm)
編程范式指我們在編寫程序解決問題的思路和視角。它提供了同時也決定了程序員對程序運行的看法。計算機編程中存在許多編程范式,如命令式編程、聲明式編程、面向對象編程以及結構化編程等等。其中面向對象編程范式認為程序是由一系列相互作用的對象組成,而結構化編程范式認為程序采用子程序、代碼區塊、for循環以及while循環等結構組成。下面主要說明本篇文章將要講到的命令式編程范式和聲明式編程范式。
1)命令式編程(Imperative):
強調程序代碼模擬電腦運行過程,強調“先做什么”、“再做什么”。如果我們要計算“2*3+1”,我們編寫代碼時先計算2*3存入臨時變量,再計算該臨時變量與1的和。命令式編程是當前主流編程范式,我們編寫的代碼幾乎都屬于命令式編程范式。
2)聲明式編程(Declarative):
強調程序代碼模擬人腦計算過程,強調“最終要什么”,相比命令式編程范式來講,它更看重結果而非過程。聲明式編程范式更接近人類思想,它的思考層面要高于命令式編程。
下圖顯示了命令式編程范式與聲明式編程范式的區別:
圖1
注:各種編程范式之間并非都是對立的,很多范式是從不同角度來劃分的。如面向對象編程范式同時也屬于命令式編程范式。當然,本篇文章講到的“命令式編程范式”和“聲明式編程范式”兩者是對立的。
聲明式編程范式
聲明式編程范式常見有以下兩種(最常見):
1)領域特定語言(Domain Specific Language,DSL):
名字很陌生,但是我們卻經常在用。如SQL、CSS以及正則表達式等等。這些語言只在特定領域起作用,并且使用這些語言時,我們大多數時候是在寫“陳述、聲明”的語句。如“select * from tb”,我們只關心我們要的結果,而不用去關系具體實現。
2)函數式編程(Functional Program,FP):
函數式編程是我們要討論的重點。既然它屬于聲明式編程范式,那么它也應該強調結果(What)而非過程(How)。沒錯,函數式編程不同于常見的命令式編程,它不關心計算機具體的實現過程,而僅僅注重問題結果。
函數式編程(Functional Program):
網上關于“函數式編程”的解釋有很多,但大多數都比較模糊抽象。維基百科上對函數式編程的解釋是“In computer science, functional programming is a programming paradigm that treats computation as the evaluation of mathematical functions and avoids state and mutable data”,翻譯成中文就是“函數式編程是一種編程范式,它將計算機運算看作是數學中函數的計算,并且避免了狀態以及變量的概念”。這是個什么意思呢?很多文章分別從函數式編程的幾個特點上做出了解釋,比如“函數是第一公民”、“高階函數(Higher Order Function)”、“無狀態性(No State)”、“無副作用性(Side-Effect)”、“易于并行開發”以及“惰性求值”等等。但是我覺得這些都只是函數式編程的特點或者說是優點,并沒有實質上解釋出“函數式編程”與普通命令式編程的區別。我認為要搞清楚函數式編程,必須先認清“函數”的概念。沒錯,雖然我們自認為我們比較熟悉“函數”(或者叫“方法”,本文不區分這兩者的區別),但是我們真的熟悉它們嗎?
注:以后博客將依次介紹“函數式編程”以上幾種特點。
編程函數和數學函數:
第一次了解“函數”的概念應該是我們讀中學時,“y=x+1”在平面坐標系中是一條直線,到后來(不知道哪年級)學習了二次函數,“y=x^2+2*x+1”在平面坐標系中是一條拋物線。當時學習函數時知道以下知識點:
1)函數是一種映射,自變量經由一種映射關系變換后,得到因變量(函數值);
2)對于每個自變量,均能、有且僅有一個因變量與之對應,這是函數的確定性。也就是說,給定一個自變量,任何時候函數值都唯一;
那么,到大學學習編程后(本人讀大學才開始學習編程),我們在程序中又遇見了“函數”,很熟悉的感覺。但是它和數學中的函數有什么關聯呢?也就是說,數學思想與我們編程思想是否有關聯?如果以我們目前寫C#、java、C++等代碼來看,它們幾乎沒有關系,因為我們程序中的函數可以沒有參數(數學函數中的“自變量”),也可以沒有返回值(數學函數中的因變量),就算一個函數有返回值,那么給定參數,調用函數后每次運行結果也可能不一樣。以上這些均不能滿足數學函數的概念。其實出現“兩種函數幾乎無關系”的現象很容易理解,數學描述的是人類思維過程,而我們(目前大部分人)編寫的程序代碼描述的是計算機運行過程。在數學家與程序員之間早已產生了溝通障礙,比如下圖:
圖2
如上圖所示,“X=X+1”這種表達式如果從數學角度來看,幾乎是不可成立的,讓任何一個沒有學過編程的人去看這個表達式,TA都會以為你寫的是錯的,他們只認“Y=X+1”。原因很簡單,在程序中,符號可以代表變量,而變量表示一個內存單元,該內存處的值可以被重寫(賦值);而在數學中,符號永遠只是符號,等號“=”兩邊表示等價關系,“Y=X+1”表示Y與X+1是等價的,Y僅僅是X+1的一個代替符號。
同理,函數也一樣。數學中的函數僅僅描述一種“映射關系”,給定一個自變量,我們可以得到一個因變量,僅此而已。而程序中的函數更多的時候扮演的是一種“功能”角色,它能夠完成指定任務。當然,如果程序中一個函數包含參數,并且能夠返回值,那么它完全可以模擬數學函數。下面使用C#編寫一個委托,它代表數學中的一個一元函數:
1 public delegate double Function1X(double x);
如上代碼所示,委托簽名中包含一個double類型參數,并且返回一個double類型返回值。數學中的“f(x)=x^2+2*x+1”可以使用C#編寫以下函數:
1 public double f(double x)2 {3 return Math.Pow(x,2) + 2*x + 1;4 }
函數f(x)在x=2處的值調用代碼:f(2);。或者使用Lambda表達式:
x => Math.Pow(x,2) + 2*x + 1;
程序中的函數接收一個double類型參數,經過映射關系,返回一個double類型的返回值,它與“f(x)=x^2 + 2*x +1”對應。那么數學函數中的二元函數在程序中怎樣表示呢?很簡單,二元函數包含兩個自變量,我們只需要為程序中函數定義兩個參數即可:
1 public delegate double Function2XY(double x,double y);
如上代碼所示,委托簽名中包含兩個double類型參數,并且返回一個double類型返回值。
從上面的介紹可以看出,如果將程序中函數做一些限制,那么它就可以模擬數學中的函數了:
1)每個函數必須包含輸入參數(作為自變量);
2)每個函數必須有返回值(作為因變量);
3)無論何時,給定參數調用函數時,返回值必須一致。
上面第三條限制是為了滿足函數的“確定性”,該條限制要求程序中的函數執行期間不能依賴于外界因素,也不要影響外部環境。換句話說,它在執行期間與外界是隔絕的。我們將滿足以上條件的函數稱為“純函數(Pure Function)”。純函數與外界交互只有一條渠道——傳入參數與返回值。純函數也不讀取/改變全局變量、無IO操作等。
圖3
純函數是程序代碼模擬數學函數的基礎。理論上講,函數式編程中,函數是第一公民的同時,所有函數也都應該屬于“純函數”。到此,我們再回過頭看一下維基百科上對“函數式編程”的解釋:函數式編程是一種編程范式,它將計算機運算看作是數學中函數的計算,并且避免了狀態以及變量的概念。很顯然,函數式編程向數學驗算靠攏,使用一種平時正常的數學思維去解決問題。
注:函數式編程是基于“lambda驗算(Lambda Calculus)”的,它并不屬于“圖靈機”理論范疇。我沒搞清楚lambda驗算,所以本文并沒詳細提到。看到Lambda很容易讓我們想到C#3.0中引入的Lambda表達式,這不是偶然。C# 3.0之后開始支持“函數式編程”,后面文章將會講到。
(未完待續)
新聞熱點
疑難解答