1. 程式人生 > >從 Racket 入門函數語言程式設計

從 Racket 入門函數語言程式設計

一直想學學LISP,今天總算開了個頭。現在學習LISP不是為了馬上能夠用於實際專案的應用,而是為了學習一下函式式的思維方式,能夠更加深入的瞭解計算的本質,能夠更好的用C++, Java, Python等編寫程式。更何況,這些主流語言都逐漸增加了函數語言程式設計的特徵,C++,Java現在都引入了 Lambda 表示式。如果能夠系統學習一下LISP,相信對自己以後掌握這些語言的新特新特徵,對自己寫JavaScript、Python,對自己瞭解閉包、高階函式、Lambda表示式都會有很大幫助。言歸正傳,首先推薦兩個資源:

Racket 的誕生與發展

簡單介紹一下Racket的發展,詳見知乎的一個關於Racket的問題回答


1958年,人工智慧之父John McCarthy 發明了一種以 Lambda 演算為基礎的符號處理語言,1960年 McCarthy 發表著名論文 Recursive Functions of Symbolic Expressions and Their Computation by Machine, 從此這種語言被命名為 LSIP (List Processor),其語法被命名為:符號表達式(S-Expression)。LISP構建在7個函式 [atom car cdr cond cons eq quote] 和2個特型 [lambda label] 之上


Lisp誕生之初是為了純粹的科學研究,程式碼執行像數學公式一樣,以人的大腦來演算。直到麥卡錫的學生斯蒂芬·羅素將eval函式在IBM 704機器上實現後,才開啟了Lisp作為一種計算機語言的歷史。1962年,第一個完整的Lisp編譯器在MIT誕生,從此之後Lisp以MIT為中心向全世界傳播。之後十多年,出現了各種Lisp方言。


1975年,Scheme誕生。Scheme同樣誕生與MIT,它的設計哲學是最小極簡主義,它只提供必須的少數幾個原語,所有其他的實用功能都由庫來實現。在極簡主義的設計思想下,Scheme趨於極致的優雅,並作為計算機教學語言在教育界廣泛使用。


1984年,Common Lisp誕生。在二十世紀七八十年代,由於Lisp方言過多,社群分裂,不利於lisp整體的發展。從1981年開始,在一個Lisp黑客組織的運作下,經過三年的努力整合後,於1984年推出了Common Lisp。由於Scheme的設計理念和其他Lisp版本不同,所以儘管Common Lisp借鑑了Scheme的一些特點,但沒有把Scheme整合進來。此後Lisp僅剩下兩支方言: Common Lisp 和 Scheme。


從二十世紀九十年代開始,由於C++、Java、C#的興起,Lisp逐漸沒落。直到2005年後,隨著科學計算的升溫,動態語言JavaScript、Python、Ruby的流行,Lisp又漸漸的回到人們的視線。不過在Lisp的傳統陣地教育界,Python作為強有力的挑戰者對Scheme發起衝鋒;在2008年,MIT放棄了使用Scheme作為教學語言的SICP(計算機程式的構造和解釋)課程,而啟用Python進行基礎教學。同時美國東北大學另立爐灶,其主導的科學計算系統PLT Scheme開始迅猛發展;2010年,PLT Scheme改名為Racket。近幾年,The Racket Language連續成為年度最活躍語言網站,並駕齊驅的還有haskell網站。

符號表達式 S-Expression

首先說一下S表示式:S-表示式的基本元素是list與atom。list由括號包圍,可包涵任何數量的由空格所分隔的元素,原子是其它內容。其使用字首表示法,在Lisp中既用作程式碼,也用作資料。如:1+2*3  寫成字首表示式就是 (+ 1 (* 2 3)) 。

  • 優點:容易parse,簡單純粹,不用考慮什麼優先順序等,也是實現程式碼即資料的前提;
  • 缺點:可讀性不是很強;

高階函式

高階函式至少滿足下列一個條件:

  1. 接受一個或多個函式作為輸入; 
  2. 輸出一個函式;

微積分中的導數就是一個例子,對映一個函式到另一個函式。在無型別 lambda 演算中,所有函式都是高階的。在函數語言程式設計中,返回另一個函式的高階函式被稱為Curry化的函式。Curry化即把接受多個引數的函式變換成接受一個單一引數(最初函式的第一個引數)的函式,並且返回接受餘下的引數而且返回結果的新函式的技術。如 f(x,y)=x+y, 如果給定了 y=1,則就得到了 g(x)=x+1 這個函式。

Lambda 表示式

Racket中實用Lambda表示式來定義匿名函式,《如何設計程式》書中給出的使用原則是:如果某個非遞迴函式只需要當引數使用一次,使用Lambda表示式。如果想用Lambda表示式來表達遞迴,就需要引入Y組合子,Y 就是這樣一個操作符,它作用於任何一個 (接受一個函式作為引數的) 函式 F,就會返回一個函式 X。再把 F 作用於這個函式 X,還是得到 X。所以 X 被叫做 F 的不動點(fixed point),即 (Y F) = (F (Y F)) 。

惰性求值

惰性求值(Lazy Evaluation),說白了就是某些中間結果不需要被求出來,求出來反而不利於後面的計算也浪費了時間。參見:惰性求值與惰性程式設計
惰性求值是一個計算機程式設計中的一個概念,它的目的是要最小化計算機要做的工作。惰性計算的最重要的好處是它可以構造一個無限的資料型別。使用惰性求值的時候,表示式不在它被繫結到變數之後就立即求值,而是在該值被取用的時候求值。語句如 x:=expression; (把一個表示式的結果賦值給一個變數)明顯的呼叫這個表示式並把計算並把結果放置到 x 中,但是先不管實際在 x 中的是什麼,直到通過後面的表示式中到 x 的引用而有了對它的值的需求的時候,而後面表示式自身的求值也可以被延遲,最終為了生成讓外界看到的某個符號而計算這個快速增長的依賴樹。

閉包

閉包在電腦科學中,閉包(Closure)是詞法閉包(Lexical Closure)的簡稱,是引用了自由變數的函式。自由變數是在表示式中用於表示一個位置或一些位置的符號,比如 f(x,y) 對 x 求偏導時,y就是自由變數。這個被引用的自由變數將和這個函式一同存在,即使已經離開了創造它的環境也不例外。在函式中(巢狀)定義另一個函式時,如果內部的函式引用了外部的函式的變數,則可能產生閉包。執行時,一旦外部的 函式被執行,一個閉包就形成了,閉包中包含了內部函式的程式碼,以及所需外部函式中的變數的引用。其中所引用的變數稱作上值(upvalue)。網上有很多講 JavaScript 閉包的文章,如果你對 LISP 有系統的瞭解,那麼這個概念自然會很清楚了。

快排的Racket實現

#lang racket
(define (quick-sort array)
  (cond
    [(empty? array) empty]                                                    ; 快排的思想是分治+遞迴
    [else (append 
           (quick-sort (filter (lambda (x) (< x (first array))) array))       ; 這裡的 array 就是閉包   
           (filter (lambda (x) (= x (first array))) array)
           (quick-sort (filter (lambda (x) (> x (first array))) array)))]))

(quick-sort '(1 3 2 5 3 4 5 0 9 82 4))
;; 執行結果 '(0 1 2 3 3 4 4 5 5 9 82)

通過這個例子,就可以感受到基於 lambda 運算元的  Racket  語言強大的表達能力了。高階函式、lambda 表示式和閉包的使用使得 Racket 所描述的快排十分的精煉,這和基於馮諾依曼模型C語言是迥然不容的思維模式。後面,隨著Racket 學習的進一步深入,嘗試寫一下直譯器。