内容简介:將實質上不同的型別,在邏輯上看成相同的型別。如 function 可能回傳若說將不同的型別,整合成單一型別。
將實質上不同的型別,在邏輯上看成相同的型別。如 function 可能回傳 int
或 bool
兩種型別,可為此 function 特別建立 IntOrBool
型別,同時包含 int
與 bool
,這就是 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-name
與 case-identifier
為必須,其他都可省略。
簡單來說, |
之後稱為 case, of
之後稱為 field
Case 不可省略,但 field 可省略
Case
Case of Primitive Type
type OrderId = | Int of int | Bool of bool
如 function 找得到資料會傳回 int
型態的的 orderId
,若找不到則傳回 bool
型態的 false
,也就是回傳型態可能是 int
或 bool
,可將此型態重新定義為 OrderId
union,則無論傳回 int
或 bool
都是 OrderId
union,且也只能傳回 int
或 bool
。
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
。
如打算將 Rectangle
、 Circle
與 Prism
三個型別定義出一個新的 Shape
union,只要是 Rectangle
或 Circle
或 Prism
之一,都算是 Shape
。
-
Rectangle
為int
*int
組合的tuple
-
Circle
為int
-
Prism
為int
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,唯沒有 new
, class
換成 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
定義好型別,如 record
或 union
,不能以 inline 的方式表示。
Field
若 case 的型別為 tuple
,雖能在 of
之後簡單的宣告 int * int
,有幾個缺點 :
-
要建立
union
時,只能依照定義順序
傳入,可讀性較差 -
無法由
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
union
與 enum
都使用 type 定義,沒有指定 int
值為 union
,有則為 enum
。
Q : F# 也有 enum
,我該用 union
還是 enum
呢 ?
F# 的 union
功能較強, enum
只是 union
的特例,實務上應優先使用 union
,除非有以下需求:
-
Case 必須搭配
int
-
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
型別。
因為 OrderId
與 CustomerId
都只能算是 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,因為 OrderId
與 CustomerId
都是具體的 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
custId
為 CustomerId
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,再由 Circle
、 EquilateralTriangle
、 Square
與 Rectangle
實踐 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()
,再由 Circle
、 EquilateralTriangle
、 Square
與 Rectangle
各自實作 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 Union》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 【1】JavaScript 基础深入——数据类型深入理解与总结
- 深入理解 Java 函数式编程,第 5 部分: 深入解析 Monad
- 深入理解 HTTPS
- 深入理解 HTTPS
- 深入浅出Disruptor
- 深入了解 JSONP
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。