1. 程式人生 > >函數式編程之-F#類型系統

函數式編程之-F#類型系統

ash rim ring cit nat oat desc 一個 car

在深入到函數式編程思想之前,了解函數式獨有的類型是非常有必要的。函數式類型跟OO語言中的數據結構截然不同,這也導致使用函數式編程語言來解決問題的思路跟OO的思路有明顯的區別。

什麽是類型?類型在編程語言中有什麽作用呢?一般來說,類型有兩個作用:

  1. 首先當你對某個數據聲明類型後,就擁有了編譯時的檢查,換句話說,你可以認為類型充當了“編譯時的單元測試”;
  2. 類型系統可以讓你建立一種模型,用來表達真實世界中的模型;

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。這樣的模型定義犯了三個錯誤:

  1. 沒有把相關一組類型組合起來,例如FirstName, MiddleName, LastName。這三個類型共同組成了Name,我們的模型並沒有體現出這樣的設計。
  2. EmailAddress真的是一個string嗎?他能有效的表達Email這樣的Domain嗎?Email分有效和無效,他擁有自己的規則,並不是所有的字符串都是Email,string這樣的類型無法表達Email的Domain含義。
  3. 在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#類型系統