作者:蔡學鏞
由於 F# 是相當新的技術,目前的學習資源不多。如果你想好好學習 F#,就必須閱讀 APress 出版的《Expert F#》,這是一本好書。我的許多解說範例取自這本書。
第一個範例程式
讓我們先來看一個例子。
#light // 分析字串內重複出現的字 let wordCount text = let words = String.split [' '] text let wordSet = Set.of_list words let nWords = words.Length let nDups = words.Length - wordSet.Count (nWords,nDups) let showWordCount text = let nWords,nDups = wordCount text printfn "--> %d words in the text" nWords printfn "--> %d duplicate words" nDups
利用 F# 的互動環境(fsi.exe),輸入上面的程式,會得到下面的結果:
val wordCount : string -> int * int val showWordCount : string -> unit
每個值都有型別
意思是,定義了兩個值(val),分別是 wordCount 與 showWordCount,它們的型別分別是「string -> int * int」與「string -> unit」。「string -> int * int」意思是:需要一個字串(string)當參數,傳出值是兩個 int 所組成的值組(tuple)。「string -> int * int」意思是:需要一個字串(string)當參數,沒有傳出值。unit 型別相當於許多編程語言的 void。
利 用 let 定義 wordCount 時,在等號前面有出現「text」,這就是字串參數;而 wordCount 定義的最後,有出現「(nWords,nDups)」,這就是傳出值,它是兩個整數(int)所組成的值組(值組的每個元素用英文逗號隔開)。定義函數的 時候,最後一個出現的值,就是傳出值。
F# 內的每個值都有型別(type),可能是 int、string 或其他,當型別中出現箭頭(->)的時候(例如上面的 wordCount),表示這個值是一個函數,箭頭左邊的是參數的型別,箭頭右邊的是傳出值的型別。如果型別中出現兩個以上的箭頭,則箭頭所隔開的每個部 分都是參數的型別,只有最右邊的是傳出值的型別,例如:「string -> int -> unit」,表示此函數的參數有兩個,分別是字串與整數,且沒有傳出值。
型別推論
你 可能會覺得奇怪,我們並未宣告 text 的型別為字串,也沒有宣告 nWords 與 nDups 的型別為整數,為何 F# 會知道它們的型別?這就是型別推論(Type Inference) -- 根據程式的內容推斷出型別。text 被當作 String.split 的第二個參數,而這個參數必須是字串,所以 F# 知道 text 是字串。而 nWords 是字串的 Length 傳回值,這個傳回值必須是 int,所以 nWords 的型別是 int。而 nDups 是兩個 int 相減的結果,自然也是 int。
透過 Type Inference,我們寫程式時不需要宣告型別,相當方便。但是某些地方,F# 如果無法順利進行 Type Inference,我們就必須標明型別,否則編譯器或互動環境會提出警告。
用 let 進行繫結
利 用 let 的語法,可以將一個名稱繫結(bind)到一個值。例如「let a = 1」表示把 a 繫結到 1。上面的例子中,我們做了七次的 let 繫結。其中只有 wordCount、showWordCount 是全域的(global),其他都是區域的(local)。因此你可以在外面使用 wordCount、showWordCount,卻不能在外面使用 words、wordSet。
要注意繫結的先後次序,例如 wordSet 有用到 words,所以要先繫結 words,再繫結 wordSet。showWordCount 有用到 wordCount,所以要先定義 wordCount,在定義 showWordCount。
特別值得注意「let nWords,nDups = wordCount text」,等號左邊是一個名稱值組,而不是一個名稱,這表示會進行模式比對(Pattern Matching),比對成功才繫結。例如「let a, b = 1, 2」,就會將 a 設定為 1,且將 b 設定為 2。
透過 #light 簡化語法
為 何 wordCount 是全域的,但 words、wordSet、nWords、nDups 卻是區域的,從上面的程式碼中不難看出,因為區域的定義都有內縮。事實上,因為程式一開始有加上 #light,表示內縮也是語法的一部份。如果沒寫 #light,那麼程式內縮也沒用,而是必須明確地在 let 敘述的最後加上「in」,才表示是區域的定義。以上面的例子來說,就是這樣:
// 沒有 #light,下面的例子內縮也沒有用,且一定要加上 in let wordCount text = let words = String.split [' '] text in let wordSet = Set.of_list words in let nWords = words.Length in let nDups = words.Length - wordSet.Count in (nWords,nDups)
當然我們會比較偏向於使用 #light 的方式。使用 #light,除了 in 之外,還有「;;」、done、begin、end 等關鍵字,都可以省略不寫,而是透過內縮表示它們之間的關係。
呼叫函數與 Property
這個範例中,我們呼叫了 String 模組(module)的 split 函數、Set 模組的 of_list 函數、還有 printfn 函數;我們也使用到 String 物件的 Length property 與 Set 物件的 Count property。不管是取用模組內的函數,或物件內的成員,都是利用「.」。
呼叫函數時,如果需要傳入參數,不需要寫括號,而是直接把參數寫在後面。例如「f a b」,表示呼叫 f,傳入「兩個」參數,a 當作第一個參數, b 當作第二個參數。如果你寫成「f (a, b)」,那麼就變成,呼叫 f,傳入「一個」參數,這是「由 a 與 b 構成的」值組。兩者的意義不同。
printfn 是格式化的輸出字串,並換行。C 語言的使用者應該相當熟悉它。
值組與清單
將元素用逗號隔開,組成的資料結構,就是值組(tuple),例如 (nWords,nDups),型別為 int * int。事實上,值組內部的元素可以是不同的型別,例如 (1, "F#") 是個值組,其型別為 int * string。
F# 也很常使用清單(list),清單內的元素型別必須一致,且使用分號隔開,前後使用方括號,例如 [ 2; 3; 5; 7; 11],型別是 int list 也可以寫成 list。本範例中,當作 String.split 第一個參數的 [' '] 其實正是一個 list,只不過它只有一個元素(空白字元' '),如果是空的清單則寫成 [ ]。
第二個範例程式
接下來看第二個範例程式:
open System.Windows.Forms let form = new Form(Visible=true,TopMost=true,Text="Welcome to F#") let textB = new RichTextBox(Dock=DockStyle.Fill, Text="Here is some initial text") form.Controls.Add(textB) open System.IO open System.Net // 取得網頁內容 let http(url: string) = let req = System.Net.WebRequest.Create(url) let resp = req.GetResponse() let stream = resp.GetResponseStream() let reader = new StreamReader(stream) let html = reader.ReadToEnd() resp.Close() html textB.Text <- http("http://news.bbc.co.uk")
使用 .NET API
這個範例中,我們使用到 .NET 的 WinForms、I/O、Networking API。再用到這些 API 之前,我們先透過 open,表示要使用它,這樣可以讓我們不用寫全名(full-qualified)。
open System.Windows.Forms open System.IO open System.Net
產生物件
產生物件一樣是透過 new 關鍵字來達成。如下所示:
let form = new Form(Visible=true,TopMost=true,Text="Welcome to F#")
從上面的例子來看,你可能以為 Form 類別有定義一個建構子(constructor),此建構子具有三個參數,分別是 boolean 表示是否 Visible、boolean 表示是否 TopMost、以及標題文字。如果你去查一下 Form 的說明文件,會找不到這樣的建構子。既然如此,上面又怎麼能這麼建構出 Form 物件呢?
從上面的例子來看,你可能以為 Form 類別有定義一個建構子(constructor),此建構子具有三個參數,分別是 boolean 表示是否 Visible、boolean 表示是否 TopMost、以及標題文字。如果你去查一下 Form 的說明文件,會找不到這樣的建構子。既然如此,上面又怎麼能這麼建構出 Form 物件呢?
let form = new Form () form.Visible <- true form.TopMost <- true form.Text <- "Welcome to F#"
設定 Property
上面用到「<-」,整個程式的最後一行也出現「<-」,這是指定(assignment)的意思。繫結(binding)和指定是不一樣的,繫結只能進行一次,使用的語法是「let a = b」,但指定可以指定多次,使用的語法「a <- b」。可以被繫結者,顯然必須是「不可被改變的」(immutable);而可以被指定者顯然必須是「可被改變的」(mutable)。.NET 物件的 Property 是可以被改變的(除非唯讀者),所以用指定的方式。
註明型別
之前提到過「型別推論」讓我們可以不必標明型別,但有些時候你想要(或需要)主動註明型別,寫法是這樣「名稱 : 名稱」。
在上面的範例定義 http 函數的時候,就有註明其參數 url 必須是字串,所以寫成這樣:
let http(url: string) = …
最後…
這篇文章利用兩個範例,詳細解說其中出現的語法,讓你對於 F# 有大概的認識。下一次的文章,會開始從最基本的一切說起,包括 F# 的各種型別、各種運算子、以及一些基本的 FP 語法。
评论