F# 程式設計入門 (1)

作者:蔡學鏞

大家對函數編程(Functional Programming,FP)相當陌生,少有人能正確地敘述出函數編程是什麼,有什麼好處。函數編程長期以來沒有出現在主流的商業軟體世界,欠缺主流語言的支援。

一直以來,真正讓 FP 無法被接受的原因可能是「執行效率」。傳統上,函數式編程語言的效率確實比命令式(imperative)編程語言來得差,這在商業系統上是不能忍受的。命令式語言讓我們用貼近馮紐曼架構(van Neumann Architecture)機器的方式寫程式,比較低階,所以效率會比較高。

但是這個原因卻有了變化。過去這十多年,我們先是對「跨平台」和「反微軟」的重視超越「執行效率」,而後又開始重視「簡單」、「快速開發」。想要「簡單」、「快速開發」,就要用比較高階的抽象,因此函數式編程比命令式編程更適合現在的開發環境。

這些年來硬體的進步,讓我們對於函數編程的效率不再是大問題;甚至由於編譯技術的進步,函數式編程語言的執行速度,現在也已經不再是吳下阿蒙。妙的是,還 不只這樣,局勢似乎 180 度反轉成為對 FP 有利的局面:多 CPU、多核心、超執行緒(HT)的硬體架構普及,以及分散式運算的流行,這根本就是專為滋養 FP 繁殖而打造的環境。

1930 年代,Alonzo Church 開發出一套 formal system,名為 lambda calculus。這個系統本質上是一個編程語言,為一部「想像中的機器」所設計的語言。lambda calculus 的函數可以接受函數當作輸入(引數)和輸出(傳出值)。這樣的函數用希臘字母 λ 當作識別,所以這個語言才名為 lambda calculus。

Alan Turing 也在做類似的研究,開發出不同的系統,就是大名鼎鼎的 Turing machine,他得到的結論和 Alonzo Church 類似。後來證實 Turing machine 和 lambda calculus 的威力一樣強大。

1958 年,對 lambda calculus 相當感興趣的 MIT 教授 John McCarthy 設計出 Lisp 語言,Lisp 實踐了 lambda calculus,讓 lambda calculus 可以在 von Neumann 電腦上執行!大家開始注意到 Lisp 的威力。1973 年,MIT 的人工智慧實驗室開發出所謂的 Lisp machine 硬體,等於是將 lambda calculus 的機器實踐出來了!

只要遵守 FP 的原則,管他用什麼語言,都可以進行 FP。你可以用非函數式的語言(例如 Java),進行 FP;正如同你可以用非物件導向的語言(例如 C),進行 OOP 一樣。但是只有想不開的人才會這麼做,畢竟事倍功半。

LISP 是第一個函數式語言,越來越多函數式語言隨之出現。真實世界的函數式語言無法像 Lambda Calculus 那樣,畢竟 Lambda Calculus 是讓虛幻不存在的機器執行的,沒有受到真實世界的限制。所以函數式語言雖然都是源自於 Lambda Calculus,但是卻都和 Lambda Calculus 之間存在差異。由於 FP 只是一些構想,各種語言實踐這些構想的作法,彼此之間也可能有不小的差異。

儘管各種語言有差異,但是大致上來說,FP 的特點在於:

  • 「沒有副作用」(Side Effect)。在表示式(expression)內不可以造成值的改變。
  • 「第一級函數」(First-Class Function)。函數被當作一般值對待,而不是次級公民,也就是說,函數可當作「傳入參數」或「傳出結果」。

基本上,遵守上述兩點進行程式編寫,差不多就可以稱為 FP。FP 和我們慣用的編寫程式風格,有相當大的差異。Imperative Programming 認為程式的執行,就是一連串狀態的改變;但 FP 將程式的運作,視為數學函數的計算,且避免「狀態」和「可變資料」。

為了提昇效率,許多函數式語言會納入 imperative 的某些作法(例如允許副作用),這類的 FPL 被稱為不純(Impure)的函數式編程語言,例如 Ocaml、F#、LISP、REBOL。當然也有一些語言堅持 Pure Functional 的作法,例如 Erlang、Haskell、Occam、Oz。

以往純的函數式語言會被某些人認為不實際,而不純的函數式語言,則被認為比較實際,但是最近大家的看法似乎有了改變。主要是以 Erlang 為首的純函數式語言,似乎更能充分展現出 FP 的優勢。FP 的優勢是容易進行單元測試、容易除錯、適合編寫(Concurrency)的程式。適合進行程式碼「熱抽換」或「熱部署」(Hot Code Deployment)。

除了上述的優點,我們可以透過 FP 作了些什麼,來瞭解 FP 是什麼。FP 語言常常能做下面的事:Higher-Order Function、Currying、Lazy Evaluation、Continuations、Pattern Matching、Closure、List Processing、Meta-Programming。如果你對這些觀念不太瞭解,可以查詢 Wikipedia 的說明。

微軟也注意到了 FP 的潮流。F# 的 F 是 Functional Language(函數式語言)的意思,# 是 .NET 的意思,所以顧名思義,F# 是 .NET 平台上函數式語言,由微軟官方所設計。F# 的血緣關係是 ML -> Caml -> OCaml(Objective-Caml)-> F#。同時 F# 也混入了一些 Haskell 和 C# 的語言特色。

F# 被視為 ML 家族系列的語言,ML 家族的函數式語言都是 Strong-Type、Static-Type,F# 也是如此。和 Common Lisp 與 Erlang 等函數式語言相比,ML 家族的語法比較正常一點,一般語言的使用者可以比較快熟悉它。

F# 雖然是編譯式語言,但是微軟有提供一個 F# 互動式 Console,使得 F# 可以用類似 Scripting 的方式編寫,也有助於學習。因此,如果你想在 Console 寫 .NET 程式,除了可以使用 PowerShell 之外,現在也可以使用 F# 了,且 F# 會比 PowerShell 更適合寫一般的 .NET 應用,因為 PowerShell 是 Shell 語言,但是 F# 是一般目的的編程語言。

Wadler 提出:大家不用函數式語言,原因有七點 Libraries、Portability、Availability、Packagability、Tools、Training、 Popularity。微軟認為 F# 可以解決這七個問題。因此函數式語言目前主要的用途雖然是以科學和財務金融領域為大宗,但是未來可能會吸引一些其他領域的人使用。F# 社群甚至有人認為,只要你用過 F# 語言,你會很難回頭去用以前的語言。我不認為這樣的魅力來自 F# 本身,我認為這樣的魅力是來自 FP(Functional Programming 函數式編程)。

F# 語言試圖整合 FP 和 .NET,讓 .NET 編程也能享有 FP 的諸多好處,但這一點我持否定的態度。想得到 FP 的好處,編寫程式時必須謹守 FP 的作法,不只是你的程式要遵守 FP,連你用到的程式庫也必須遵守 FP 才行,而 .NET Framework 是 OOP + Imperative Programming 的方式設計出來的 API,並不是依據FP的構想而設計出來的 API。

但不要因此小看了 F# 未來的潛力。微軟特別為 F# 在 .NET Framework 上做出一套逼近 OCaml 3.06 的 ML 相容程式庫,以及 F# 專屬的程式庫。寫程式時盡量多使用這些程式庫,少用標準的 .NET 框架,就比較能享有 FP 的優點。例如:使用 ParallelFX,而不要用 .NET Threading API。

你可能會問:既然如此,為何不直接用 OCaml 就好了?因為 F# 是微軟的語言,以微軟豐富的資源,如果有意好好發展 F#,F# 的未來是無可限量的。

F# 雖然強調 FP,但它其實是多重範式(multi-paradigm)的語言,不只是 FP 的語言。FP 的範式包括了 Functional Programming、Imperative Programming、Object-Oriented Programming、Meta-Programming。多重範式的好處是,你可以依據當時的需求,使用最適合的範式;缺點是你往往挑錯範式。因此一般人用 F# 開發出來的程式,能享有多少 FP 範式的優點,這是相當值得懷疑的。

讓我們瞭解一下 F# 語言有哪些優點和缺點。除了 FP 的優點都可以算到 F# 頭上之外,有微軟撐腰也是 F# 的一大優點。微軟已經進行 F# 的開發許多年了,看得出微軟對 F# 是認真的。F# 的缺點是目前似乎還在演化中、還是有一些 bug、目前還在實驗室產品和商業化產品的過渡期、文件寫得不完備。

由於文件寫得不完備,所以想利用網站上的資源學習 F#,會比較辛苦一些。妙的是,寫得比文件更完整的書出卻已經出版了。其中一本「Expert F#」似乎頗受好評,作者是 F# 計畫的最重要負責人 Don Syme。

F# 目前(2008 年五月)最新版是 1.9.4,你可以在 F# 網站上取得。下載回來之後,依照它的說明安裝,你就可以開始使用 F# 了。提醒你,安裝 F# 之前,你需要先安裝 .NET 2.0 的環境。

安裝完 F# 之後,你將具有 F# 程式庫、F# 編譯器 FSC.EXE、F# 互動環境 FSI.EXE。如果你有 Visual Studio,你也可以依照它的說明步驟,讓 F# 和 Visual Studio 整合在一起。用 Visual Studio 寫 F# 程式當然比用記事本 + 命令列來得方便。

提醒你,要先將你的 F# 安裝路徑下的 bin 目錄,加入 PATH 環境變數中。才可以進行後續的動作。

現在就讓我們用記事本和命令列,以最儉樸的方式寫一個 F# 程式:


let x = "Hello World";;
System.Console.WriteLine(x);;

你應該也注意到了,F# 用兩個分號「;;」當作結尾。習慣上,F# 的副檔名是 .fs 或 .ml。將上面的檔案存檔為 Hello.fs。接下來,用下面的方式編譯:


let x = "Hello World";; System.Console.WriteLine(x);; 

編譯成功之後,你會看到 hello.exe 檔案。這是 .NET 的 Managed PE 檔。讓我們執行這個檔案:


C:\>Hello.exe
Hello World 

上面是編譯器的執行方式,接下來,看看如何使用解譯器的執行方式,做同樣的事。執行 FSI,進入它的 Console,做下面的輸入:


> let x = "Hello World";;
val x = string
> System.Console.WriteLine(x);; 
Hello World
val it : unit = ()

顯然每一行程式都會立刻執行,並立刻做出反應。這對於初學者來說,是相當方便的一種學習方式。

在執行完「let x = "Hello World";;」之後,你會看到系統輸出「val x = string」,意思是,現在有一個變數 x,它被繫結(bind)到一個字串。

在執行完「System.Console.WriteLine(x);;」之後,你會看到系統輸出「Hello World」,這是執行結果,然後又輸出「val it : unit = ()」,意思是,現在有一個變數 it,它被繫結(bind)到一個 unit。如果你沒有指定變數,那麼 FSI 會自動將它繫結到變數 it。

離開 FSI 的方式有三種:

  • #quit;;
  • #q;;
  • exit 0;;

F# 可以用來寫一般的 .NET 程式,下面是 F# 版本的 WinForm 程式:


#light
open System
open System.Windows.Forms

let form = new Form()
form.Width  <- 400
form.Height <- 300
form.Visible <- true 
form.Text <- "Hello World Form"

// Menu bar, menus 
let mMain = form.Menu <- new MainMenu()
let mFile = form.Menu.MenuItems.Add("&File")
let miQuit  = new MenuItem("&Quit")
mFile.MenuItems.Add(miQuit)

// RichTextView 
let textB = new RichTextBox()
textB.Dock <- DockStyle.Fill  
textB.Text <- "Hello World\n\nOK."
form.Controls.Add(textB)    

// callbacks 
miQuit.Click.Add(fun _ -> form.Close())

#if COMPILED
// Run the main code. The attribute marks the startup application thread as "Single 
// Thread Apartment" mode, which is necessary for GUI applications. 
[]    
do Application.Run(form)
#endif

如果你懂 WinForm 的話,這個程式應該不需要我的解說。我只說明一些和 F# 有關的重點。一開始的「#light」讓你可以不用在每行程式的最後中加入「;;」。「#light」也會讓「程式內縮」變成語法的一部份(而不只是為了美觀而已)。open 相當於 import。為 property 指定值要使用「<-」運算子。「miQuit.Click.Add(fun _ -> form.Close())」的「fun _ -> form.Close()」就是一個匿名函數,也就是 lambda 函數。函數可以當作 Add 的引數,所以 Add 就是 Higher-Order Function(較高次方函數)。

從上面的程式來看,你可能會感到疑惑,F# 好像和 C# 的差異也不是很大,並沒有特別精簡,這是因為你用 F# 寫 .NET 程式的緣故。如果你使用 F# 自己的程式庫「FSharp.Core」組件與「FSharp.Compatibility」組件,狀況就會不同了。

大多數的人沒有使用過函數編程技術,所以思維會受到傳統 imperative 編程作法的拉扯,一開始很不習慣。只要堅持下去,跨過門檻之後,你會發現,函數編程其實更自然,生產力更高。下次的文章開始介紹 F# 的語法。

顯示: