函數式編程之-F#類型系統
在深入到函數式編程思想之前,了解函數式獨有的類型是非常有必要的。函數式類型跟OO語言中的數據結構截然不同,這也導致使用函數式編程語言來解決問題的思路跟OO的思路有明顯的區別。
什麽是類型?類型在編程語言中有什麽作用呢?一般來說,類型有兩個作用:
- 首先當你對某個數據聲明類型後,就擁有了編譯時的檢查,換句話說,你可以認為類型充當了“編譯時的單元測試”;
- 類型系統可以讓你建立一種模型,用來表達真實世界中的模型;
Tuple type
元組是函數式編程語言中的常用類型,同時在.NET 4.0中被引入到了C#中。但是在C#中幾乎不太會用到這種類型,但是在函數式編程語言中卻隨處可見。
例如在C#中這樣使用元組:
var s = new Tuple<int, int>(1, 2);
var fist = s.Item1;
在F#中
let t = 1,2
let first = fst t //提取第一個元素
let second =snd t //提取第二個元素
let f s = t //通過解構提取元素
Record type
Record type是F#中最常用的類型。經常被用來對領域建模(Domain modeling)。例如定義一個矩形數據結構:
type Rect = { Left: float32 Top: float32 Width: float32 Height: float32 }
這看起來跟OO語言中對class的定義是很相似的,好比在某個class中聲明了一組屬性。使用起來也簡單:
let rc = {
Left = 10.0f;
Top = 10.0f;
Width = 200.0f;
Height = 200.0f
}
你可以把它當做一個簡單的class,但是從定義和使用都能看出來Record type更加簡單和直接一些。並且我們並沒有在Record type裏設計一些方法,這根class有本質的區別。
Record type還支持“復制一個現有記錄並進行一些修改”:
let rc2 ={ rc with Left = rc.Left + 100.0f }
C#中的class是沒有這種能力的,你不得不顯示復制所有屬性。
另外Record type自動實現了equal操作符:
type Name = { First:string ; Last:string}
let jim = { First ="Jim"; Last = "Dan"}
let jim2 = {First = "Jim"; Last = "Dan"}
let isSame = jim = jim2 //true
使用Record type來建立領域模型
考慮下面的Contact領域模型:
type Contact = {
FirstName: string;
MiddleName: string;
LastName: string;
EmailAddress: string;
Address1: string;
Address2: string;
City: string;
State: string;
Zip: string;
}
如果你把它當做一個class也是可行的,實際上在OO語言裏我們也經常設計這樣的class。這樣的模型定義犯了三個錯誤:
- 沒有把相關一組類型組合起來,例如FirstName, MiddleName, LastName。這三個類型共同組成了Name,我們的模型並沒有體現出這樣的設計。
- EmailAddress真的是一個string嗎?他能有效的表達Email這樣的Domain嗎?Email分有效和無效,他擁有自己的規則,並不是所有的字符串都是Email,string這樣的類型無法表達Email的Domain含義。
- 在F#中沒有null,如果你認為某個類型可能為空,就應該設計為option類型,例如MiddleName,他應該是string option。
還記得前面的章節我們說函數式編程的核心思想是組合
,組合不但體現在函數之間的組合,類型也是可組合的:
type PersonalName = {
FirstName: string;
MiddleName: string option;
LastName: string;
}
type EmailContactInfo = {
EmailAddress: string;
IsEmailVerified: bool;
}
type PostalAddress = {
Address1: string;
Address2: string;
City: string;
State: string;
Zip: string;
}
type PostalContactInfo = {
Address: PostalAddress;
IsAddressValid: bool;
}
type Contact = {
Name: PersonalName;
EmailContactInfo: EmailContactInfo;
PostalContactInfo: PostalContactInfo;
}
Descriminated Unions type
中文翻譯過來叫做可區分聯合
,這種類型試圖來為不同的選項進行建模,所以你可以把他理解為選項類型
。
舉個例子:“現在溫度是多少?“
如何對現在的溫度建模?你問的是攝氏度呢還是華氏度呢?如果是攝氏度即38°,如果單位是華氏度,則為100.4°。
type Temperature =
| F of float
| C of int
let tempNow = C(30)
let tempNow2 = F(100.4)
只有一個選項的類型:
type EmailAddress = EmailAddress of string
let email = "a" |> EmailAddress
let emails = ["a"; "b"; "c"] |> List.map EmailAddress
使用 Descriminated Unions type來建立領域模型
選項類型
在F#是非常常用的領域模型建模類型,比如設計一個關於支付的模型,在OO語言中,你可能會這樣做:
interface IPaymentMethod { }
class Cash : IPaymentMethod { }
class Cheque: IPaymentMethod { }
class Card : IPaymentMethod { }
...
在函數式語言中利用選項類型
可以輕松搞定:
type PaymentMethod =
| Cash
| Cheque of ChequeNumber
| Card of CardType * CardNumber
OO思想中通過抽象接口和定義派生類來實現這個模型。函數式語言則利用選項類型
把模型核心內容通過盡可能少的代碼展現出來。
此時你也許會有所疑慮,在OO的設計中,每種支付方式都是一個獨立的實現,所以每種支付方式的具體行為就可以設計在具體的實現中,例如Payment的過程。不同的支付方式顯然需要不同的支付過程,這種設計在OO中的好處是顯而易見的。
在選項類型
中,似乎把三種不同的支付方式揉在了一塊,那麽每種支付方式的支付過程這種行為怎麽實現呢?答案是模式匹配
,我們將在下節介紹。
函數式編程之-F#類型系統