函数式编程语言:LISP/Scheme 小语种简介
1、概述
自從裘宗燕教授翻譯了《計算機程序的構造和解釋》(Structure and Intepretation of Computer Programs,SICP)第二版之后,這本MIT計算機系的編程入門教材開始越來越多地受到中國開發者的關注。同時受到關注的,還有它所介紹的函數式編程(Functional Programming),以及其中范例所使用的Scheme語言。
時光倒轉到30年前,1975年,Bill Gates和Paul Allen寫出了那個傳奇的BASIC版本——他們后來賣給MITS、換來“第一桶金”的那個BASIC版本。同一年,Gerald Sussman——他正是SICP的作者——創造了Scheme語言。其實Scheme并不是一種新鮮的語言,準確地說,它只是LISP的一個變體、一種方言。早在1958年,John McCarthy就開始研究一種“用于處理列表數據”的語言——這也是LISP名字的由來(LISt Processing)?!?strong>列表處理”乍看上去是一個相當特定的問題,但實際上這類問題有著深遠而重要的內涵,稍后我們就會看到這里的故事。
?
2、Scheme 語言特性與實例介紹
與LISP的其他方言相比,Scheme最大的特點或許在于:它是可以被編譯成機器碼的。也就是說,它的運行效率更高。除此之外,Scheme在語言層面上可說是中規中矩。LISP素來奉仰的哲學思想是“微核心+高擴展性”,Scheme也將這一特點發揮到了極致。Scheme內置的關鍵字(keyword)少得可憐,就連大于小于、加減乘除等操作都是以函數的形式出現。甚至可以夸張一點說,只要有define關鍵字與括號,就可以寫出所有程序。不過,這種風格的一個副作用是會在程序中出現大量的括號,所以也有人把LISP戲稱為“一大堆煩人的、教人看不懂的括號”(Lots of Irritating, Spurious Parentheses)。譬如說,下面的程序是用來求一個值的平方:
(define (square x)
??????(* x x))
(display (square 3))
對于我們這些從C語言開始入門、習慣了過程式編程(相對于“函數式編程”而言)的程序員,初接觸LISP/Scheme之時,受到的第一個觸動大概就是:Scheme不區分數據與操作。仍然以“求平方”的例子,“x的平方”可以表述為“以1為基數,將‘乘以x’計算兩次”。如果用C++語言,這個邏輯可以這樣實現:
int square(int x) {
??????return 1 * x * x;
}
而在Scheme中,我們還可以這樣實現:
(define (twice func base arg)
??????(func base (func base arg)))
(define (square x)
??????(twice * 1 x))
這種實現的特點在哪里?最大的特點就是:一個操作(乘法運算)被當作參數傳遞。按照程序設計的“黑話”,如果一個程序單元可以被作為參數和返回值傳遞,那么這個單元就被稱為“一等公民”(first class)。在C/C++/Java等語言中,雖然也可以通過函數指針、functor等形式傳遞“操作”,但畢竟是經過了包裝;而在Scheme中,可以將另一個函數直接作為參數傳入函數,也可以作為返回值傳回另一個函數,函數(也即“操作”)完全被作為一等公民對待。
這樣做的好處是什么?在上面的例子中,我們把“兩次執行某操作”的邏輯也抽象出來,得到了twice函數。如果我們想要實現“以0為基數將加法操作執行兩遍”(也就是“乘以2”),只需要這樣寫:
(define (double x)
??????(twice + 0 x))
這里的twice函數是“對別的函數進行操作的函數”,它的結果取決于傳入什么函數給它作為參數。像這種“函數的函數”,在函數式編程的術語中被稱為“高階函數”(high-order)。能夠很自然地實現高階函數,是Scheme的第二個重要特點。在前面已經提到過,LISP這個名字代表“列表處理”,其實處理列表數據的能力正是來自對高階函數的運用。譬如說,我們有下面這樣一個列表:
{1, 2, 3, 4, 5}
針對這個列表,我們要做兩件事:
1.??????將每個元素翻倍,得到新的列表:{2, 4, 6, 8, 10}
2.??????對每個元素求平方,得到新的列表:{1, 4, 9, 16, 25}
用Java語言,我們可以這樣實現:
List<int> doubleList(List<int> src) {
??????List<int> dist = new ArrayList<int>();
??????for(int item : src) {
????????????dist.add(item * 2);
}
return dist;
}
List<int> squareList(List<int> src) {
??????List<int> dist = new ArrayList<int>();
??????for(int item : src) {
????????????dist.add(item * item);
}
return dist;
}
問題一目了然:除了加粗的兩行代碼之外,這兩個方法幾乎是完全重復的。細想之下,其實這兩個方法做的事情非常相似:遍歷一個列表,按照“某種規則”將原列表的每個元素映射到新的列表中。因為這個“某種規則”是針對元素的映射操作,所以為了抽象這種列表操作,我們必須實現一個高階函數,將實際的映射操作以參數的形式傳入。于是,在能夠方便實現高階函數的Scheme中,以上邏輯實現起來相當容易:
(define (double-list src)
??????(map double src))
(define (square-list src)
??????(map square src))
由于高階函數在操作邏輯抽象方面的強大與便利,很多人開始尋求在“主流”的過程式語言中將操作當作一等公民對待,進而實現高階函數。譬如說,C#以delegate的形式允許將方法作為參數或返回值傳遞,并且在List<T>類型中加入了find等高階操作;Java世界的FunctionalJ(http://functionalj.sourceforge.net/)則在Java5提供的泛型基礎上提供了filter、map等常用的列表操作。前面的例子如果用FunctionalJ來實現,就可以寫成:
// double and square are Function instances
List<int> doubleList(List<int> src) {
??????return Functions.map(double, src);
}
List<int> squareList(List<int> src) {
return Functions.map(square, src);
}
?
3、優勢與不足
“不區分數據與操作”這句話說起來很簡單,其實背后蘊含著一個重要的哲學問題,即“什么是時間”的問題。按照過程式編程的理念,“時間”是操作內部的一個變量,程序以局部變量的形式記錄系統在各個時間點的瞬時狀態;而按照函數式編程的理念,“時間”是操作外部的變量,以參數的形式傳入函數,函數內部則沒有局部狀態,更沒有賦值操作?;蛘吒唵我稽c說,在任何時候用同樣的參數調用同一個函數,必定會得到同樣的結果。這種性質被稱為“引用透明”。如果操作不具備引用透明性,就不能將它作為參數或返回值傳遞,因為調用環境和順序都可能改變高階函數的結果。
具備引用透明性的程序還有一項額外的好處:它們天生地具有線程安全性。不論有多少條線程、以什么順序訪問,只要程序具有引用透明性,就不需要額外的線程同步機制來保證結果正確。在同時面向眾多用戶的服務器端應用、尤其是web應用中,這一點顯得特別重要。Rod Johnson在他的《J2EE Development without EJB》一書中提倡“無狀態的Java服務器端應用”,企業應用的開發者們也從函數式編程的思想中受益匪淺。
據說LISP當初的發明頗有些無心插柳的味道:McCarthy只實現了一個基于lambda運算的抽象語法就把它扔到一邊,而他的學生們卻發現在這樣極簡的語法中寫程序別有一番樂趣。有人說計算機科學家是一群喜歡歸約的人,LISP的發明卻是從實踐的角度證明:基本上所有的程序結構都可以歸約為lambda運算。按照Alonzo Church發明的丘奇代數(Church Calculus)理論,一切可以有效計算的函數——包括定值函數——都可以用lambda運算來定義。譬如說,數據“0”和操作“加1”可以用lambda運算定義如下:
(define zero (lambda (f) (lambda (x) x)))
(define (add-1 n)
? (lambda (f) (lambda (x) (f ((n f) x)))))
在此基礎上,就可以用lambda運算定義整個自然數系。這是一個極端的例子。在別的很多地方,LISP/Scheme也能夠以類似的方式剖析我們習以為常的概念,讓我們獲得更加深入的洞見。譬如在SICP第二章“構造數據抽象”中,我們親眼看到:平時所說的“面向過程編程”與“面向對象編程”,在很大程度上無非是使用同一組lambda運算的不同語法糖而已。熟悉面向對象的程序員在學習Scheme時,常常會因為跨越了“數據”與“操作”的鴻溝而獲得一些全新的理解。再加上Scheme的語法極其簡單,最常用的關鍵字大概不超過5個,所以作為教學語言有著得天獨厚的優勢——不少學校采用Java作為大學生的編程入門語言,當你看著這些可憐的學生在兩個月之后還在與“匿名內部類”之類詭異的語法和“IO流裝飾器”之類復雜的類庫糾纏不清時,你不難理解我的意思。
但這種簡單性也成為Scheme在企業應用中推廣的最大障礙:企業應用需要的不是許多種優雅的可能性,而是一種可行的解決方案。雖然PLT等Scheme實現版本提供了XML、servlet等工具庫,但過于靈活的語法、最佳實踐的缺乏、以及沒有大廠商的支持,都讓Scheme終于無法成為企業應用的主流。不過,盡管幾乎沒有真正的應用,來自函數式編程的理念還是啟發著企業應用的開發者們。譬如WebWork2.2引入的continuation特性,就是來自函數式編程的概念。
最后——但并非最不重要的——應該說明:雖然很少在企業應用中見到蹤影,但LISP/Scheme在科學計算、人工智能、數學建模等領域的運用非常廣泛,所以把它稱作“小語種”多少有些不太公平。整體而言,LISP/Scheme強于算法邏輯的編寫,而不善于I/O操作。我們當然可以說這是它失意于企業應用領域的原因,但又何嘗不可以是它的結果呢?
總結
以上是生活随笔為你收集整理的函数式编程语言:LISP/Scheme 小语种简介的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【Python】Matplotlib绘制
- 下一篇: 【Java】《Java编程的逻辑》第3章