基本范式
人生最偉大的目標是行動 ——《洛克菲勒的忠告》
第二課伊始,冒號開門見山:“首先介紹的是最基本的兩種編程范式:命令式和聲明式,其中命令式又稱過程式。通俗點說,命令式編程由命令序列組成,即一系列祈使句:‘先做這,再做那’,強調‘怎麼做’;聲明式編程由相關表達式組成,即一系列陳述句:‘已知這,求解那’,強調‘做什麼’。學術點說,命令式編程是電腦(von Neumann機)運行機制的抽象,即有序地從內存中獲取指令和數據然後去執行;聲明式編程是人腦思維方式的抽象,即利用數理邏輯或既定規范(specification)對已知條件進行推理運算。”
引號嘟囔著:“大多數語言好像都是命令式的。”
冒號接口道:“語言的演化是漸進的,大多數語言追根溯源是匯編語言的升級,而作為與機器語言一一對應的匯編語言自然是命令式的,因而這種范式最為傳統和普及。聲明式語言則發轫於人工智能的研究,主要包括函數式語言和邏輯式語言。其實它們的出現並不比命令式的晚多少——最早的函數式語言Lisp(LISt Processor)已有半個世紀的歷史,最早之一的邏輯式語言Prolog(PROgramming in LOGic)也與C同齡。只是由於大多數更多地用於學術研究而非商業應用,頗有些‘養在深閨人未識’。起源的不同決定了這兩大類范式代表著迥然不同的編程理念和風格:命令式編程是行動導向(Action-Oriented)的,因而算法是顯性而目標是隱性的;聲明式編程是目標驅動(Goal-Driven)的,因而目標是顯性而算法是隱性的。為便於說明,下面分別用三種代表性的語言來實現階乘(factorial)運算。”
冒號在黑板上打出投影——
C(命令式):
int factorial(int n)
{
int f = 1;
for (; n > 0; --n) f *= n;
return f;
}
Lisp(函數式):
(defun factorial(n)
(if (= n 0) 1 // 若n等於0,則n!等於 1
(* n (factorial(- n 1))))) // 否則n!等於n* (n-1)
Prolog(邏輯式):
// 0! 等於1
factorial(0,1).
// 若M等於N-1且 M!等於Fm且F等於N*Fm,則N! 等於F
factorial(N,F) :- M is N-1, factorial(M,Fm), F is N * Fm.
冒號提問:“撇開語法細節,大家說說以上三段代碼區別在哪裡?”
句號沉思片刻,答道:“C明確給出了階乘的迭代算法,而Lisp僅描述了階乘的遞歸定義,Prolog則陳述了兩個關於階乘的斷言。”
冒號很滿意:“一語中的!第二個問題:你們更習慣哪一種思維方式?”
逗號不加思索:“當然是第一種!”
冒號微笑著說:“這證明你至少是受過一定訓練的程序員。大家回想一下,當你們初學編程時,是否習慣這種思維方式?”
歎號沉吟道:“好像不太習慣i = i + 1之類的語句。”
“對!”冒號的一嗓子嚇了眾人一跳,“我們最早接觸的變量是代數方程中的x、y、z等,本質上是抽象化的符號,變量值是該符號在給定約束條件下的允許值。而命令式編程中的變量本質上是抽象化的內存,變量值是該內存的儲存內容。通俗地說,前者好比姓名,所指之人是固定的;後者好比住址,所住之人是變化的。此外,等號在代數中是一種約束,而在許多命令式語言中則表示賦值。因此i = i + 1可以在命令式編程中出現,但絕不可能在數學推理中出現——除非在反證法中。”
歎號又道:“現在回頭再看代數,反倒有些不習慣了。”
“這就是思維的定勢效應。”冒號感慨道,“聲明式編程讓我們重回數學思維,其中函數式編程類似代數中的表達式變換和計算,邏輯式編程則類似數理邏輯推理。其中的變量也如數學中的一樣,是抽象符號而非內存地址,因此沒有賦值運算,不會產生變量被改寫的副作用,也不存在內存分配和釋放的問題。這既簡化了代碼,也減少了調試——不妨想一想,有多少bug是由於某個變量被意外改寫或內存管理不慎而造成的?”
問號問道:“聲明式語言與命令式語言看來是兩個世界的產物,它們有何相通之處?”
冒號答道:“首先,所有高級語言都建立於低級語言之上,最終轉化為機器語言,聲明式語言也不例外。其次,聲明式語言與命令式語言並非泾渭分明,而是互相交叉滲透的。一些‘非純粹’ 的聲明式語言也提供變量賦值和流程控制,而一些命令式語言也在逐漸發展,通過調用其他軟件或增加新的語言特征來實現聲明式編程。總的說來,在命令式語言中融入聲明式的元素應當是一種趨勢,尤其是函數式,它的一些特征已經在許多命令式語言中得到了支持。比較而言,聲明式編程重目標、輕過程,專注問題的分析和表達而不致陷入算法的迷宮,其代碼也更加簡潔清晰、易於修改和維護。”
逗號仍然有些疑惑:“既然聲明式編程有這麼多好處,為什麼命令式語言不僅占大多數,而且流行程度也不減呢?”
冒號回答:“編程語言的流行程度與其擅長的領域關系密切。聲明式語言擅長基於數理邏輯的應用,如人工智能、符號處理、數據庫、編譯器等,對基於業務邏輯的、尤其是交互式或事件驅動型的應用就不那麼得心應手了。而大多數軟件是面向用戶的,交互性強、多為事件驅動、業務邏輯千差萬別,顯然命令式語言在此更有用武之地。”
大家頻頻颔首。
問號突然想到了什麼,指著投影問:“這裡用Lisp實現階乘的方法不也可以用在C上嗎?”
冒號點點頭,寫下一段代碼——
int factorial(int n)
{
return n == 0 ? 1 : n * factorial(n - 1);
}
“這是C的遞歸實現。”冒號娓娓道來,“除了細微的語法差別外,二者的確很相似,這說明用命令式語言也可以講出聲明式的味道。實際上,命令式語言提倡迭代而不鼓勵遞歸,早期的Fortran 甚至都不支持遞歸。一則迭代比遞歸更符合命令式的思維模式,因為前者貼近機器語言而後者貼近數學語言;二則除尾遞歸(tail recursion)外,一般遞歸比迭代的開銷(overhead)大。相反,聲明式語言提倡遞歸而不支持迭代。就語法而言,它不允許迭代中的循環變量;就視角而言,迭代著眼微觀過程而遞歸著眼宏觀規律。”
歎號輕歎:“原來貌似普通的迭代和遞歸有那麼多道道!”
“任何語言都難脫命令式或聲明式的窠臼,因此上述三種范式最為基本。”冒號歸納道,“歸根結底,編程是尋求一種機制,將指定的輸入轉化為指定的輸出。三種范式對此提供了迥然不同的解決方案:命令式把程序看作一個自動機,輸入是初始狀態,輸出是最終狀態,編程就是設計一系列指令,通過自動機執行以完成狀態轉變;函數式把程序看作一個數學函數,輸入是自變量,輸出是因變量,編程就是設計一系列函數,通過表達式變換以完成計算;邏輯式把程序看作一個邏輯證明,輸入是題設,輸出是結論,編程就是設計一系列命題,通過邏輯推理以完成證明。繪成表格如下——”
范式 程序 輸入 輸出 程序設計 程序運行 命令式 自動機 初始狀態 最終狀態 設計指令 命令執行 函數式 數學函數 自變量 因變量 設計函數 表達式變換 邏輯式 邏輯證明 題設 結論 設計命題 邏輯推理
冒號見眾人微顯難色,寬慰道:“這部分理論性稍微強了些,對函數式和邏輯式也僅作了描述性說明,並未深入。不解之處,大可不必介懷,撒下的種子總有一天會萌動。先休息片刻,不要走開,廣告之後更精彩。”