深入探討 F# 之 Discriminated Union

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

内容简介:將實質上不同的型別,在邏輯上看成相同的型別。如 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》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

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

复杂:信息时代的连接、机会与布局

复杂:信息时代的连接、机会与布局

罗家德 / 中信出版集团股份有限公司 / 2017-8-1 / 49.00 元

信息科技一方面创造了人们互联的需要,另一方面让人们在互联中抱团以寻找归属感,因此创造了大大小小各类群体的认同和圈子力量的兴起,即互联的同时又产生了聚群,甚至聚群间的相斥。要如何分析这张网?如何预测它的未来变化?如何在网中寻找机会,实现突围?本书提出了4个关键概念──关系、圈子、自组织与复杂系统: • 关系 关系是人与人的连接,又可以被分为强关系和弱关系。强关系就是和你拥有亲密关系的人,......一起来看看 《复杂:信息时代的连接、机会与布局》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具

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

HSV CMYK互换工具