程式設計語言的發展趨勢及未來方向(3):函數式編程

來源:互聯網
上載者:User

參見:http://blog.zhaojie.me/2010/05/trends-and-future-directions-in-programming-languages-by-anders-3-functional-programming-and-fsharp.html

這是Anders Hejlsberg(不用介紹這是誰了吧)在比利時TechDays 2010所做的開場演講。由於最近我在部落格上關於語言的討論比較多,出於應景,也打算將Anders的演講完整地聽寫出來。在上一部分中,Anders闡述了他眼中聲明式編程的理念及DSL,並示範C#中一種內部DSL的形式:LINQ。在這一部分中,Anders談及了聲明式編程的另一個重要組成部分:函數式編程,並使用.NET平台上的函數式程式設計語言F#進行了示範。

如果沒有特別說明,所有的文字都直接翻譯自Anders的演講,並使用我自己的口語習慣表達出來,對於Anders的口誤及反覆等情況,必要時在譯文中自然也會進行忽略。為了方便理解,我也會將視頻中關鍵區段進行,而某些代碼示範則會直接作為文章內容發表。

(聽寫開始,接上篇)

 

關於聲明式編程的還有一部分重要的內容,那便是函數式編程。函數式編程已經有很長時間的曆史了,當年LISP便是個函數式程式設計語言。除了LISP以外我們還有其他許多函數式程式設計語言,如APL、Haskell、Scheme、ML等等。關於函數式編程在學術界已經有過許多研究了,在大約5到10年前許多人開始吸收和整理這些研究內容,想要把它們融入更為通用的程式設計語言。現在的程式設計語言,如C#、Python、Ruby、Scala等等,它們都受到了函數式程式設計語言的影響。

 

我想在這裡先花幾分鐘時間簡單介紹一下我眼中的函數式程式設計語言。我發現很多人聽說過函數式程式設計語言,但還不十分清楚它們和普通的命令式程式設計語言究竟有什麼區別。如今我們在使用命令式程式設計語言寫程式時,我們經常會寫這樣的語句,嗨,x等於x加一,此時我們大量依賴的是狀態,可變的狀態,或者說變數,它們的值可以隨程式運行而改變。

可變狀態非常強大,但隨之而來的便是叫做“副作用”的問題。在使用可變狀態時,你的程式則會包含副作用,比如你會寫一個無需參數的void方法,然後它會根據你的調用次數或是在哪個線程上進行調用對程式產生影響,因為void方法會改變程式內部的狀態,從而影響之後的運行效果。

而在函數式編程中則不會出現這個情況,因為所有的狀態都是不可變的。你可以聲明一個狀態,但是不能改變這個狀態。而且由於你無法改變它,所以在函數式編程中不需要變數。事實上對函數式編程的討論更像是數學、公式,而不像是程式語句。如果你把x = x + 1這句話交給一個程式員看,他會說“啊,你在增加x的值”,而如果你把它交給一個數學家看,他會說“嗯,我知道這不是true”。

 

然而,如果你給他看這條語言,他會說“啊,y等於x加一,就是把x + 1的計算結果交給y,你是為這個計算指定了一個名字”。這時候在思考時就是另一種方式了,這裡y不是一個變數,它只是x + 1的名稱,它不會改變,永遠代表了x + 1。

所以在函數式程式設計語言中,當你寫了一個函數,接受一些參數,那麼當你調用這個函數時,影響函數調用的只是你傳進去的參數,而你得到的也只是計算結果。在一個純函數式程式設計語言中,函數在計算時不會對進行一些神奇的改變,它只會使用你給它的參數,然後返回結果。在函數式程式設計語言中,一個void方法是沒有意義的,它唯一的作用只是讓你的CPU發熱,而不能給你任何東西,也不會有副作用。當然現在你可能會說,這個CPU發多少熱也是一個副作用,好吧,不過我們現在先不討論這個問題。

 

這裡的關鍵在於,你解決問題的方法和以前大不一樣了。我這裡還是用代碼來說明問題。使用函數式語言寫沒有副作用的代碼,就好比在Java或C#中使用final或是readonly的成員。

例如這裡,我們有一個Point類,建構函式接受x和y,還有一個MoveBy方法,可以把一個點移動一些位置。 在傳統的命令式編程中,我們會改變Point執行個體的狀態,這麼做在平時可能不會有什麼問題。但是,如果我把一個Point對象同時交給3個API使用,然後我修改了Point,那麼如何才能告訴它們狀態改變了呢?可能我們可以使用事件,blablabla,如果我們沒有事件,那麼就會出現那些不愉快的副作用了。

那麼使用函數式編程的形式寫代碼,你的Point類還是可以包含狀態,例如x和y,不過它們是readonly的,一旦初始化以後就不能改變了。MoveBy方法不能改變Point對象,它只能建立一個新的Point對象並返回出來。這就是一個建立新Point對象的函數,不是嗎?這樣就可以讓調用者來決定是使用新的還是舊的Point對象,但這裡不會有產生副作用的情況出現。

在函數式編程裡自然不會只有Point對象,例如我們會有集合,如Dictionary,Map,List等等,它們都是不可變的。在函數式編程中,當我們向一個List裡添加元素時,我們會得到一個新的List,它包含了新增的元素,但之前的List依然存在。所以這些資料結構的實現方式是有根本性區別的,它們的內部結構會設法讓這類操作變的儘可能高效。

在函數式編程中訪問狀態是十分安全的,因為狀態不會改變,我可以把一個Point或List對象交給任意多的地方去訪問,完全不用擔心副作用。函數式編程的十分容易並行,因為我在運行時不會修改狀態,因此無論多少線程在運行時都可以觀察到正確的狀態。兩個函數完全無關,因此它們是並行還是順序地執行便沒有什麼區別了。我們還可以有延遲計算,可以進行Memorization,這些都是函數式編程中十分有趣的方面。

你可能會說,那麼我們為什麼不都用這種方法來寫程式呢?嗯,最終,就像我之前說的那樣,我們不能只讓CPU發熱,我們必須要把計算結果表現出來。那麼我們在螢幕上列印內容時,或者把資料寫入檔案或是Socket時,其實就產生了副作用。因此真實世界中的函數式編程,往往都是把純粹的部分進行隔離,或是進行更細緻的控制。事實上也不會有真正純粹的函數式程式設計語言,它們都會帶來一定的副作用或是命令式編程的能力。但是,它們預設是函數式的,例如在函數式程式設計語言中,所有東西預設都是不可變的,你必須做些額外的事情才能使用可變狀態或是產生危險的副作用。此時你的編程觀念便會有所不同了。

 

我們在自己的環境中開發出了這樣一個函數式程式設計語言,F#,已經包含在VS 2010中了。F#誕生於微軟劍橋研究院,由Don Syme提出,他在F#上已經工作了5到10年了。F#使用了另一個函數式程式設計語言OCaml的常見核心部分,因此它是一個強型別語言,並支援一些如模式比對,類型推斷等現代函數式程式設計語言的特性。在此之上,F#又增加了非同步工作流程,度量單位等較為前沿的語言功能。

而F#最為重要的一點可能是,在我看來,它是第一個和工業級的架構和工具集,如.NET和Visual Studio,有深入整合的函數式程式設計語言。F#允許你使用整個.NET架構,它和C#也有類似的執行期特徵,例如強型別,而且都會產生高效的代碼等等。我想,現在應該是展示一些F#代碼的時候了。

 

首先我想先從F#中我最喜歡的特性講起,這是個F#命令列……(開啟命令列視窗以及一個F#源檔案)……F#包含了一個互動命令列,這允許你直接輸入代碼並執行。例如輸入5……x等於5……然後x……顯示出x的值是5。然後讓sqr x等於x乘以x,於是我這裡定義了一個簡單的函數,名為sqr。於是我們就可以計算sqr 5等於25,sqr 10等於100。

F#的使用方式十分動態,但事實上它是一個強型別的程式設計語言。我們再來看看這裡。這裡我定義了一個計算平方和的函數sumSquares,它會遍曆每個列表中每個元素,平方後再把它們相加。讓我先用命令式的方式編寫這個函數,再使用函數式的方式,這樣你可以看出其中的區別。

let sumSquaresI l =     let mutable acc = 0    for x in l do        acc <- acc + sqr x    acc

這裡先是命令式的代碼,我們先建立一個累加器acc為0,然後遍曆列表l,把平方加到acc中,然後最後我返回acc。有幾件事情值得注意,首先為了建立一個可變的狀態,我必須顯式地使用mutable進行聲明,在預設情況下這是不可變的。

 

還有一點,這段代碼裡我沒有提供任何的類型資訊。當我把滑鼠停留在方法上時,就會顯示sumSquaresI方法接受一個int序列作為參數並返回一個int。你可能會想int是哪裡來的,嗯,它是由類型推斷而來的。編譯器從這裡的0發現acc必須是一個int,於是它發現這裡的加號表示兩個int的相加,於是sqr函數返回的是個int,再接下來blablabla……最終它發現這裡到處都是int。

 

如果我把這裡修改為浮點數0.0,滑鼠再停留一下,你就會發現這個函數接受和返回的類型都變成float了。所以這裡的類型推斷功能十分強大,也十分方便。

 

現在我可以選擇這個函數,讓它在命令列裡執行,然後調用sumSquaresI,提供1到100的序列,就能得到結果了。

let rec sumSquaresF l =     match l with    | [] -> 0    | h :: t -> sqr h + sumSquaresF t

那麼現在我們來換一種函數式的風格。這裡是另一種寫法,可以說是純函數式的實現方式。如果你去理解這段代碼,你會發現有不少數學的感覺。這裡我定義了sumSqauresF函數,輸入一個l列表,然後使用下面的模式去匹配l。如果它為空白,則結果為0,否則把列表匹配為頭部和尾部,然後便將頭部的平方和尾部的平方和相加。

你會發現,在計算時我不會去改變任何一個變數的值,我只是建立新的值。我這裡會使用遞迴,就像在數學裡我們經常使用遞迴,把一個公式分解成幾個變化的形式,以此進行遞迴的定義。在編程時我們也使用遞迴的做法,然後編譯器會設法幫我們轉化成尾遞迴或是迴圈等等。

於是我們便可以執行sumSquaresF函數,也可以得到相同的結果。當然實際上可能你並不會像之前這樣寫代碼,你可能會使用高階函數:

let sumSquares l = Seq.sum (Seq.map (fun x -> x * x) l )

例如這裡,我只是把函數x乘以x映射到列表上,然後相加。這樣也可以得到相同的結果,而且這可能是更典型的做法。我這裡只是想說明,這個語言在編程時可能會給你帶來完全不同的感受,雖然它的執行期特徵和C#比較接近。

這便是關於F#的內容。

(未完待續)

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.