深入探討 F# 之 Discriminated Union

栏目: ASP.NET · 发布时间: 7年前

内容简介:將實質上不同的型別,在邏輯上看成相同的型別。如 function 可能回傳若說將不同的型別,整合成單一型別。

將實質上不同的型別,在邏輯上看成相同的型別。如 function 可能回傳 intbool 兩種型別,可為此 function 特別建立 IntOrBool 型別,同時包含 intbool ,這就是 Discriminated Union,簡稱 union

若說 tuple 是將不同型別加以 AND,則 union 是將不同型別加以 OR。

Definition

將不同的型別,整合成單一型別。

Q : 為什麼要稱為 Discriminated Union?

因為不是簡單的將不同型別加以 union 而已,而可以將不同的型別取 case-identifier 加以區別 (discriminated),所以稱為 Discriminated Union。

Syntax

[ attributes ]
type type-name =
    | case-identifier1 [of [ fieldname1 : ] type1 [ * [ fieldname2 : ] type2 ...]
    | case-identifier2 [of [fieldname3 : ]type3 [ * [ fieldname4 : ]type4 ...]
...
  • type-name : 定義 union 的型別名稱
  • I : 因為 union 包含多種型別,所以使用 | ,第一個 | 可省略
  • case-identifider : 為每個型別取一個別名,必須為 大駝峰
  • of : 每個別名的型別,可以是內建型別,也可以是自己定義的其他 type
  • * : 若 of 之後為 tuple ,不同 type 以 * 區別不同型別
  • fieldname : 若 of 型別是 tuple ,可為 tuple 內每個型別取別名

以上只有 type-namecase-identifier 為必須,其他都可省略。

簡單來說, | 之後稱為 case, of 之後稱為 field

Case 不可省略,但 field 可省略

Case

Case of Primitive Type

type OrderId = 
  | Int of int
  | Bool of bool

如 function 找得到資料會傳回 int 型態的的 orderId ,若找不到則傳回 bool 型態的 false ,也就是回傳型態可能是 intbool ,可將此型態重新定義為 OrderId union,則無論傳回 intbool 都是 OrderId union,且也只能傳回 intbool

type OrderId = Int of int | Bool of bool

若 case 很少,也可以寫成一行,則第一個 | 可省略。

Case of Unnamed Type

type Shape =
    | Rectangle of int * int
    | Circle of int
    | Prism of int * int * int

of 之後的型別,如是 unnamed type,可以直接 inline 表示,如直接指定為 tuple

如打算將 RectangleCirclePrism 三個型別定義出一個新的 Shape union,只要是 RectangleCirclePrism 之一,都算是 Shape

  • Rectangleint * int 組合的 tuple
  • Circleint
  • Prismint int int 組合的 tuple

type * type 為 tuple 的型別定義方式

type MixedType = 
    Tuple of int * int
    List of int list

Collection 也屬於 unnamed type,亦可直接 inline 表示。

int list 表示為 list 型別,其 element 型別為 int

let rectangle = Rectangle (1, 2)
let circle = Circle 1
let prism = Prism (1, 2, 3)

當建立 union 時,以類似 constructor 的方式建立,稱為 case constructor,唯沒有 newclass 換成 case ,且必須要照 定義順序 傳入。

type C = 
    | Circle of int 
    | Rectangle of int * int

[1..10]
|> List.map Circle

[1..10]
|> List.zip [21..30]
|> List.map Rectangle

Case contructor 本質就是 function,因此任何可傳入 function 之處,就可傳入 case constructor。

Case of Named Type

type Person = { first: string; last: string }
type IntOrBool = Int of int | Bool of bool

type MixedType = 
    | Person of Person       
    | IntOrBool of IntOrBool
  
type MixedType = 
    | Person of  { first: string; last: string }  // error
    | IntOrBool of (Int of int | Bool of bool)    // error

of 之後的型別若是 named type,則必須先用 type 定義好型別,如 recordunion ,不能以 inline 的方式表示。

Field

若 case 的型別為 tuple ,雖能在 of 之後簡單的宣告 int * int ,有幾個缺點 :

  1. 要建立 union 時,只能依照 定義順序 傳入,可讀性較差
  2. 無法由 tuple 看出其 domain 上的意義

若我們加上 field,則清楚許多。

type Shape =
    | Rectangle of width : int * length : int
    | Circle of radius: int
    | Prism of width: int * length : int * height: int

of 之後加上 field,可明確表達出 tuple 的每個 element 的 domain 意義。

let rectangle = Rectangle (length = 1, width = 2)
let circle = Circle (radius = 1)
let prism = Prism (width = 1, length = 2, height = 3)

建立 union 時,可在 case constructor 明確指定其 field,如此可讀性更高,且不用依照 定義順序 傳入。

Empty Case

type Directory = 
    | Root                   
    | Subdirectory of string

type Result = 
    | Success                
    | ErrorMessage of string

Case 並不一定要搭配 type,若該 case 並不需要任何型態的值傳入,可以不指定 type。

let myDir1 = Root
let myDir2 = Subdirectory "bin"

let myResult1 = Success
let myResult2 = ErrorMessage "not found"

沒有 type 的 case,其 case constructor 就不用傳入任何值。

type Size = Small | Medium | Large
let mySize = Small

當全部 case 都沒有 type 時,其功能等效於 enum

type Size = Small     | Medium     | Large     // DU
type Size = Small = 0 | Medium = 1 | Large = 2 // enum

unionenum 都使用 type 定義,沒有指定 int 值為 union ,有則為 enum

Q : F# 也有 enum ,我該用 union 還是 enum 呢 ?

F# 的 union 功能較強, enum 只是 union 的特例,實務上應優先使用 union ,除非有以下需求:

  1. Case 必須搭配 int
  2. union 必須與其他 .NET 語言搭配時

才必須使用 enum

F# 的 enum 與 .NET 的 enum 是相同的

Single Case

雖然 union 原本的用途是用在將不同的型別整合成單一型別,也就是將不同的 case 整合成一個 union ,但實務上有一種應用是一個 union 只有一個 case,所謂的 single case。

type CustomerId = int
type OrderId = int

let printOrderId (orderId: OrderId) = 
   printfn "The orderId is %i" orderId

let custId = 1
printOrderId custId

第 1 行

type CustomerId = int
type OrderId = int

type 能對 primitive type 取 alias,所以我們分別對 int 定義成 CustomerId type 與 OrderId type。

第 4 行

let printOrderId (orderId: OrderId) = 
   printfn "The orderId is %i" orderId

建立 printOrderId function,傳入參數的型別為 OrderId

第 7 行

let custId = 1
printOrderId custId

custId 的型別為 int ,傳入 printOrderId compiler 也沒報錯,明明要的是 OrderId 型別。

因為 OrderIdCustomerId 都只能算是 int 的 alias,還不算是個型別。

type CustomerId = CustomerId of int
type OrderId = OrderId of int

let printOrderId (OrderId orderId) =
   printfn "The orderId is %i" orderId

let custId = CustomerId 1
printOrderId custId                   // Error

第 1 行

type CustomerId = CustomerId of int
type OrderId = OrderId of int
  • 定義 CustomerId union,其 case 為 CustomerId ,型別為 int
  • 定義 OrderId union,其 case 為 OrderId ,型別為 int

當使用 single case 的 union 時,type 會與 case 相同

第 4 行

let printOrderId (OrderId orderId) =
   printfn "The orderId is %i" orderId

建立 printOrderId function,傳入參數的型別為 OrderId

與之前的 printOrderId function 一樣。

第 7 行

let custId = CustomerId 1
printOrderId custId                   // Error

custId 型別不再是 int ,而是 CustomerId ,因為使用了 CustomerId 的 case constructor 建立。

custId 傳入 printOrderId 後,如願出現 compiler error,因為 OrderIdCustomerId 都是具體的 type,而不只是 alias。

Destructor

let getShapeHeight shape =
    match shape with
    | Rectangle(height = h) -> h
    | Circle(radius = r) -> 2. * r
    | Prism(height = h) -> h

union 傳入 function 後,可使用 Pattern Matching 與 field 將 tuple 的值取出。

with 之後配合的 union 的 case, () 內配合 field,可以直接取出該 field 的值。

使用 field 之後,可輕易的配合 Pattern Matching 取出 tuple 內的值

let getCustomerId (CustomerId customerId) = 
    printfn "The CustomerId is %i" customerId

在 function 的 paramter 使用 () ,將 case 寫在 parameter 之前,則自動會將傳入的 union destruct 成 value。

語法雖然很類似 C#,但別忘了 F# 的 type 是在 : 之後,所以 CustomerId 寫在前面並不是型別,而是 union 的 case

let (CustomerId customerId) = custId 
let CustomerId customerIdInt = custId // error

custIdCustomerId union,會直接 destruct 成 customerId

使用 destructor 時,一定要加上 () ,否則會誤以為是新的 function

Equality

type Contact = 
    | Email of string 
    | Phone of int

let email1 = Email "bob@example.com"
let email2 = Email "bob@example.com"

let areEqual = (email1=email2) // true

雖然 union 為 reference type,但 union 的比較卻像 value type,只要 type 一樣,value 一樣, union 就算一樣。

Representation

type Contact = Email of string | Phone of int
let email = Email "bob@example.com"

printfn "%A" email    // nice

printfn 使用 %A 支援 union

Object Hierarchy

type Shape =
    | Circle of float
    | EquilateralTriangle of double
    | Square of double
    | Rectangle of double * double

若使用 OOP,會設計 Shape interface,再由 CircleEquilateralTriangleSquareRectangle 實踐 Shape ,如此需要開 5 個檔案。

若使用 union ,只要 5 行就可解決。

let pi = 3.141592654

let area myShape =
    match myShape with
    | Circle radius -> pi * radius * radius
    | EquilateralTriangle s -> (sqrt 3.0) / 4.0 * s * s
    | Square s -> s * s
    | Rectangle (h, w) -> h * w

若使用 OOP,由於各種形狀計算面積的公式不同,勢必在 Shape interface 開 area() ,再由 CircleEquilateralTriangleSquareRectangle 各自實作 area()

但在 FP 的 F#,只需使用 pattern matching 根據 union 的不同 case 實作即可,6 行即可解決。

Conclusion

  • F# 的 union 非常強大,可以算是 enum 的威力加強版,搭配 Pattern Matching 更是如虎添翼
  • union 配合 tuple 可以定義出複雜的 domain model
  • Single case 的 union 可以替 domain 定義一個更有意義的型別名稱,且兼具 type safety 與 compiler 保護

Reference

F# , Discriminated Unions

F# for fum and profit , Discriminated Unions


以上所述就是小编给大家介绍的《深入探討 F# 之 Discriminated Union》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Head First Python

Head First Python

Paul Barry / O'Reilly Media / 2010-11-30 / USD 49.99

Are you keen to add Python to your programming skills? Learn quickly and have some fun at the same time with Head First Python. This book takes you beyond typical how-to manuals with engaging images, ......一起来看看 《Head First Python》 这本书的介绍吧!

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具