編程語言可以分成兩類:
- 命令式
- 聲明式
事實上,凡是非命令式的編程都可歸為聲明式編程。因此,命令式、函數式和邏輯式是最核心的三種范式。為清楚起見,我們用一幅圖來表示它們之間的關系。

與命令式編程相對的聲明式編程(declarative programming)。顧名思義,聲明式編程由若干規范(specification)的聲明組成的,即一系列陳述句:‘已知這,求解那’,強調‘做什么’而非‘怎么做’。聲明式編程是人腦思維方式的抽象,即利用數理邏輯或既定規范對已知條件進行推理或運算。
聲明式編程的發源
聲明式編程發軔於人工智能的研究,主要包括函數式編程(functional programming,簡稱FP)和邏輯式編程(logic programming,簡稱LP)。其中,函數式編程將計算描述為數學函數的求值,而邏輯式編程通過提供一系列事實和規則來推導或論證結論。
其實支持它們的語言出現得並不比命令式的晚多少——最早的函數式語言Lisp(LISt Processor)已有半個世紀的歷史,最早之一的邏輯式語言Prolog(PROgramming in LOGic)也與C同齡。只是由於大多數更多地用於學術研究而非商業應用,頗有些‘養在深閨人未識’的味道。
起源的不同決定了這兩大類范式代表着迥然不同的編程理念和風格:命令式編程是行動導向(Action-Oriented)的,因而算法是顯性而目標是隱性的;聲明式編程是目標驅動(Goal-Driven)的,因而目標是顯性而算法是隱性的。為便於說明,我們分別用三種代表性的語言來實現階乘(factorial)運算。
階乘的三種編程實現
C(命令式)——
1 |
int factorial(int n) |
2 |
{ |
3 |
int f = 1; |
4 |
for (; n > 0; --n) f *= n; |
5 |
return f; |
6 |
} |
Lisp(函數式)——
1 |
(defun factorial(n) |
2 |
( if (= n 0) |
3 |
1 // 若n等於0,則n!等於1 |
4 |
(* n (factorial(- n 1))))) // 否則n!等於n* (n-1) |
Prolog(邏輯式)——
1 |
// 0! 等於1 |
2 |
factorial(0,1). |
3 |
// 若M等於N-1且 M!等於Fm且F等於N*Fm,則N! 等於F |
4 |
factorial(N,F) :- M is N-1, factorial(M,Fm), F is N * Fm. |
以上三段代碼區別在哪里?C明確給出了階乘的迭代算法,而Lisp僅描述了階乘的遞歸定義,Prolog則陳述了兩個關於階乘的斷言。
聲明式編程的本質
我們最早接觸的變量是代數方程中的x、y、z等,本質上是抽象化的符號,變量值是該符號在給定約束條件下的允許值。而命令式編程中的變量本質上是抽象化的內存,變量值是該內存的儲存內容。通俗地說,前者好比姓名,所指之人是固定的;后者好比住址,所住之人是變化的。此外,等號在代數中是一種約束,而在許多命令式語言中則表示賦值。因此 i = i + 1 可以在命令式編程中出現,但絕不可能在數學推理中出現 —— 除非在反證法中。
聲明式編程讓我們重回數學思維:函數式編程類似代數中的表達式變換和計算,邏輯式編程則類似數理邏輯推理。其中的變量也如數學中的一樣,是抽象符號而非內存地址,因此沒有賦值運算,不會產生變量被改寫的副作用(side-effect),也不存在內存分配和釋放的問題。這既簡化了代碼,也減少了調試——不妨想一想,有多少bug是由於某個變量被意外改寫或內存管理不慎而造成的?
聲明式語言與命令式語言的相通之處
- 首先,所有高級語言都建立於低級語言之上,最終轉化為機器語言,聲明式語言也不例外。
- 其次,聲明式語言與命令式語言並非涇渭分明,而是互相交叉滲透的。一些‘非純粹’ 的聲明式語言也提供變量賦值和流程控制,而一些命令式語言也在逐漸發展,通過利用其他程序或增加新的語言特征來實現聲明式編程。
總的說來,在命令式語言中融入聲明式的元素應當是一種趨勢。尤其是函數式,它的一些特征已經在許多命令式語言中得到了支持。比較而言,聲明式編程重目標、輕過程,專注問題的分析和表達而不致陷入算法的迷宮,其代碼也更加簡潔清晰、易於修改和維護。從這種意義上說,聲明式語言天然地就比命令式語言更高級。
-
既然聲明式編程有這么多好處,為什么命令式語言不僅占大多數,而且流行程度也不減呢?
-
編程語言的流行程度與其擅長的領域關系密切。聲明式語言——尤其是函數式語言和邏輯式語言——擅長基於數理邏輯的應用,如人工智能、符號處理、數據庫、編譯器等,對基於業務邏輯的、尤其是交互式或事件驅動型的應用就不那么得心應手了。而大多數軟件是面向用戶的,交互性強、多為事件驅動、業務邏輯千差萬別,顯然命令式語言在此更有用武之地。
值得指出的是,聲明式編程並不僅僅局限於函數式和邏輯式。比方說,C#中的attribute、Java中的annotation和XDoclet庫等采用的也是具有聲明式特征的屬性導向式編程(Attribute-Oriented Programming,簡稱@OP)。再比如,Prograph、SISAL等數據流語言(dataflow language)采用的數據流式編程(Dataflow Programming)與函數式編程有不少共同點,同樣屬於聲明式的范疇。還有一些語言如Oz、CHIP等支持與邏輯式編程相交的約束式編程(Constraint Programming)。此外,大家熟悉的數據庫語言SQL,樣式語言XSLT、CSS,標記語言HTML、XML、SVG,規范語言IDL(Interface Description Language)等等都是聲明式的。算上它們,聲明式語言所占的比例也是非常可觀的。此前之所以沒有提及,一方面,不少聲明式語言采用的范式並沒有專門的名稱;另一方面,這些語言大多是領域特定語言,並且不少並非圖靈完備的,有的連運算都沒有。畢竟,目前我們的重點還是放在通用編程語言上。
其實用Lisp實現階乘的方法也可以用在C上:
1 |
int factorial(int n) |
2 |
{ |
3 |
return n == 0 ? 1 : n * factorial(n - 1); |
4 |
} |
這是C的遞歸實現。除了細微的語法差別外,二者的確很相似,這說明用命令式語言也可以講出聲明式的味道。實際上,命令式語言提倡迭代而不鼓勵遞歸,早期的Fortran 甚至都不支持遞歸。一則迭代比遞歸更符合命令式的思維模式,因為前者貼近機器語言而后者貼近數學語言;二則除尾遞歸(tail recursion)外,一般遞歸比迭代的開銷(overhead)大。相反,聲明式語言提倡遞歸而不支持迭代。就語法而言,它不允許迭代中的循環變量;就視角而言,迭代着眼微觀過程而遞歸着眼宏觀規律。
具體可以看看這個:漫談遞歸
歸根結底,編程是尋求一種機制,將指定的輸入轉化為指定的輸出。三種范式對此提供了截然不同的解決方案:
- 命令式把程序看作一個自動機,輸入是初始狀態,輸出是最終狀態,編程就是設計一系列指令,通過自動機執行以完成狀態轉變;
- 函數式把程序看作一個數學函數,輸入是自變量,輸出是因變量,編程就是設計一系列函數,通過表達式變換以完成計算;
- 邏輯式把程序看作一個邏輯證明,輸入是題設,輸出是結論,編程就是設計一系列命題,通過邏輯推理以完成證明。
繪成表格如下:
范式 | 程序 | 輸入 | 輸出 | 程序設計 | 程序運行 |
---|---|---|---|---|---|
命令式 | 自動機 | 初始狀態 | 最終狀態 | 設計指令 | 命令執行 |
函數式 | 數學函數 | 自變量 | 因變量 | 設計函數 | 表達式變換 |
邏輯式 | 邏輯證明 | 題設 | 結論 | 設計命題 | 邏輯推理 |
http://www.nowamagic.net/academy/detail/1220525
- 命令式編程通過一系列改變程序狀態的指令來完成計算,聲明式編程只描述程序應該完成的任務。命令式編程模擬電腦運算,是行動導向的,關鍵在於定義解法,即“怎么做”,因而算法是顯性而目標是隱性的;聲明式編程模擬人腦思維,是目標驅動的,關鍵在於描述問題,即“做什么”,因而目標是顯性而算法是隱性的。
- 函數式編程通過數學函數的表達式變換和計算來求值。
- 邏輯式編程通過一系列事實和規則,利用數理邏輯來推導或論證結論。
- 命令式編程中的變量代表抽象化的內存,所存內容可能改變。聲明式編程中的變量代表抽象化的符號,所指對象一般不會改變。
- 聲明式編程專注問題的分析和表達而不是算法實現,不用指明執行順序,一般沒有或極少副作用,也不存在內存管理問題。這些都大大降低了編程的復雜度,同時也非常適合於並發式計算。
- 編程語言的流行程度與其擅長的領域密切相關。函數式語言和邏輯式語言擅長基於數理邏輯的應用,命令式語言擅長基於業務邏輯的、尤其是交互式或事件驅動型的應用。
- 聲明式語言與命令式語言之間並無絕對的界限,它們均建立於低級語言之上,並且互相滲透融合。
- 在命令式語言中引入函數或過程,是一種向聲明式風格的趨近。
- 編程是尋求一種機制,將指定的輸入轉化為指定的輸出。
- 三種核心編程范式采用如下不同的機制:
- 命令式:自動機機制,通過設計指令完成從初始態到最終態的轉變。
- 函數式:數學變換機制,通過設計函數完成從自變量到因變量的計算。
- 邏輯式:邏輯證明機制,通過邏輯推理完成從題設到結論的證明。