跳轉到內容

F# 程式設計/區分聯合

來自華夏公益教科書,開放的書籍,開放的世界
上一頁:集合和對映 索引 下一頁:可變資料
F#:區分聯合

區分聯合,也稱為標記聯合,表示一組有限且定義明確的選擇。區分聯合通常是構建更復雜資料結構(包括連結列表和各種樹)的首選工具。

建立區分聯合

[編輯 | 編輯原始碼]

區分聯合使用以下語法定義

type unionName =
    | Case1
    | Case2 of datatype
    | ...

按照慣例,聯合名稱以小寫字母開頭,聯合情況用帕斯卡大小寫書寫。

聯合基礎:開關的開/關

[編輯 | 編輯原始碼]

假設我們有一個電燈開關。對於大多數人來說,電燈開關只有兩種可能的狀態:電燈開關可以開啟,也可以關閉。我們可以使用 F# 聯合來模擬我們的電燈開關的狀態,如下所示

type switchstate =
    | On
    | Off

我們定義了一個名為 switchstate 的聯合,它有兩個情況,OnOff。您可以建立和使用 switchstate 的例項,如下所示

type switchstate =
    | On
    | Off
    
let x = On (* creates an instance of switchstate *)
let y = Off (* creates another instance of switchstate *)
    
let main() =
    printfn "x: %A" x
    printfn "y: %A" y
    
main()

此程式具有以下型別

type switchstate = On | Off
val x : switchstate
val y : switchstate

它輸出以下內容

x: On
y: Off

請注意,我們透過使用其情況的名稱來建立一個 switchstate 例項;這是因為,從字面上講,聯合的情況是建構函式。您可能已經猜到,由於我們使用相同的語法來構建物件和匹配物件,我們可以透過以下方式對聯合進行模式匹配

type switchstate =
    | On
    | Off
    
let toggle = function (* pattern matching input *)
    | On -> Off
    | Off -> On
    
let main() =
    let x = On
    let y = Off
    let z = toggle y
    
    printfn "x: %A" x
    printfn "y: %A" y
    printfn "z: %A" z
    printfn "toggle z: %A" (toggle z)
    
main()

函式 toggle 的型別為 val toggle : switchstate -> switchstate

此程式的輸出如下

x: On
y: Off
z: On
toggle z: Off

在聯合中儲存資料:調光開關

[編輯 | 編輯原始碼]

上面的示例故意保持簡單。事實上,在許多方面,上面定義的區分聯合與列舉值並沒有太大區別。但是,假設我們想將我們的電燈開關模型更改為調光開關的模型,或者換句話說,是一個允許使用者將燈泡的功率輸出從 0% 調整到 100% 的電燈開關。

我們可以修改上面的聯合來適應三種可能的狀態:開啟、關閉和一個在 0 到 100 之間的可調值

type switchstate =
    | On
    | Off
    | Adjustable of float

我們添加了一個新的情況,Adjustable of float。此情況從根本上與其他情況相同,只是它在建構函式中接受一個 float

open System

type switchstate =
    | On
    | Off
    | Adjustable of float
    
let toggle = function
    | On -> Off
    | Off -> On
    | Adjustable(brightness) ->
        (* Matches any switchstate of type Adjustable. Binds
        the value passed into the constructor to the variable
        'brightness'. Toggles dimness around the halfway point. *)
        let pivot = 0.5
        if brightness <= pivot then
            Adjustable(brightness + pivot)
        else
            Adjustable(brightness - pivot)

let main() =
    let x = On
    let y = Off
    let z = Adjustable(0.25) (* takes a float in constructor *)
    
    printfn "x: %A" x
    printfn "y: %A" y
    printfn "z: %A" z
    printfn "toggle z: %A" (toggle z)
    
    Console.ReadKey(true) |> ignore
    
main()

此程式輸出

x: On
y: Off
z: Adjustable 0.25
toggle z: Adjustable 0.75

建立樹

[編輯 | 編輯原始碼]

區分聯合可以輕鬆地模擬各種樹和層次資料結構。

例如,讓我們考慮以下二叉樹

Binary tree with 5 leaf nodes

我們樹的每個節點都包含正好兩個分支,每個分支可以是整數或另一棵樹。我們可以用以下方式表示這棵樹

type tree =
    | Leaf of int
    | Node of tree * tree

我們可以使用以下程式碼建立上面樹的例項

open System

type tree =
    | Leaf of int
    | Node of tree * tree
    
let simpleTree =
    Node(
        Leaf 1,
        Node(
            Leaf 2,
            Node(
                Node(
                    Leaf 4,
                    Leaf 5
                ),
                Leaf 3
            )
        )
    )
    
let main() =
    printfn "%A" simpleTree
    Console.ReadKey(true) |> ignore
    
main()

此程式輸出以下內容

Node (Leaf 1,Node (Leaf 2,Node (Node (Leaf 4,Leaf 5),Leaf 3)))

通常,我們希望遞迴地列舉樹結構中的所有節點。例如,如果我們想計算樹中 Leaf 節點的總數,我們可以使用

open System

type tree =
    | Leaf of int
    | Node of tree * tree

let simpleTree =
    Node (Leaf 1, Node (Leaf 2, Node (Node (Leaf 4, Leaf 5), Leaf 3)))
    
let rec countLeaves = function
    | Leaf(_) -> 1
    | Node(tree1, tree2) ->
        (countLeaves tree1) + (countLeaves tree2)

let main() =
    printfn "countLeaves simpleTree: %i" (countLeaves simpleTree)
    Console.ReadKey(true) |> ignore
    
main()

此程式輸出

countLeaves simpleTree: 5

對所有資料型別泛化聯合

[編輯 | 編輯原始碼]

請注意,我們上面的二叉樹只對整數起作用。可以構造對所有可能的資料型別起作用的聯合。我們可以將我們聯合的定義修改為以下內容

type 'a tree =
    | Leaf of 'a
    | Node of 'a tree * 'a tree

(* The syntax above is "OCaml" style. It's common to see
   unions defined using the ".NET" style as follows which
   surrounds the type parameter with <'s and >'s after the 
   type name:

type tree<'a> =
    | Leaf of 'a
    | Node of tree<'a> * tree<'a>
*)

我們可以使用上面定義的聯合來定義任何資料型別的二叉樹

open System

type 'a tree =
    | Leaf of 'a
    | Node of 'a tree * 'a tree  
    
let firstTree =
    Node(
        Leaf 1,
        Node(
            Leaf 2,
            Node(
                Node(
                    Leaf 4,
                    Leaf 5
                ),
                Leaf 3
            )
        )
    )
    
let secondTree =
    Node(
        Node(
            Node(
                Leaf "Red",
                Leaf "Orange"
            ),
            Node(
                Leaf "Yellow",
                Leaf "Green"
            )
        ),
        Node(
            Leaf "Blue",
            Leaf "Violet"
        )
    )
    
let prettyPrint tree =
    let rec loop depth tree =
        let spacer = new String(' ', depth)
        match tree with
        | Leaf(value) ->        
            printfn "%s |- %A" spacer value
        | Node(tree1, tree2) ->
            printfn "%s |" spacer
            loop (depth + 1) tree1
            loop (depth + 1) tree2
    loop 0 tree
    
let main() =
    printfn "firstTree:"
    prettyPrint firstTree
    
    printfn "secondTree:"
    prettyPrint secondTree
    Console.ReadKey(true) |> ignore
    
main()

上面的函式具有以下型別

type 'a tree =
    | Leaf of 'a
    | Node of 'a tree * 'a tree

val firstTree : int tree
val secondTree : string tree
val prettyPrint : 'a tree -> unit

此程式輸出

firstTree:
 |
  |- 1
  |
   |- 2
   |
    |
     |- 4
     |- 5
    |- 3
secondTree:
 |
  |
   |
    |- "Red"
    |- "Orange"
   |
    |- "Yellow"
    |- "Green"
  |
   |- "Blue"
   |- "Violet"

內建聯合型別

[編輯 | 編輯原始碼]

F# 有幾種從區分聯合派生的內建型別,其中一些已經在本教程中介紹過。這些型別包括

type 'a list =
    | Cons of 'a * 'a list
    | Nil

type 'a option =
    | Some of 'a
    | None

命題邏輯

[編輯 | 編輯原始碼]

ML 語言家族(包括 F# 及其母語 OCaml)最初是為開發自動定理證明器而設計的。聯合型別允許 F# 程式設計師以非常簡潔的方式表示命題邏輯。為了簡單起見,讓我們將命題限制為四種可能的情況

type proposition =
    | True
    | Not of proposition
    | And of proposition * proposition
    | Or of proposition * proposition

假設我們有一系列命題,並希望確定它們是否計算為真或假。我們可以透過以下方式遞迴地列舉命題語句來輕鬆編寫 eval 函式

let rec eval = function
    | True -> true
    | Not(prop) -> not (eval(prop))
    | And(prop1, prop2) -> eval(prop1) && eval(prop2)
    | Or(prop1, prop2) -> eval(prop1) || eval(prop2)

eval 函式的型別為 val eval : proposition -> bool

以下是用 eval 函式的完整程式

open System
 
type proposition =
    | True
    | Not of proposition
    | And of proposition * proposition
    | Or of proposition * proposition
 
let prop1 =
    (* ~t || ~~t *)
    Or(
        Not True,
        Not (Not True)
    )
 
let prop2 =
    (* ~(t && ~t) || ( (t || t) || ~t) *)
    Or(
        Not(
            And(
                True,
                Not True
            )
        ),
        Or(
            Or(
                True,
                True
            ),
            Not True
        )
    )
 
let prop3 =
    (* ~~~~~~~t *)
    Not(Not(Not(Not(Not(Not(Not True))))))
 
let rec eval = function
    | True -> true
    | Not(prop) -> not (eval(prop))
    | And(prop1, prop2) -> eval(prop1) && eval(prop2)
    | Or(prop1, prop2) -> eval(prop1) || eval(prop2)
 
let main() =
    let testProp name prop = printfn "%s: %b" name (eval prop)
    
    testProp "prop1" prop1
    testProp "prop2" prop2
    testProp "prop3" prop3
 
    Console.ReadKey(true) |> ignore
 
main()

此程式輸出以下內容

prop1: true
prop2: true
prop3: false

其他閱讀

[編輯 | 編輯原始碼]

定理證明示例(OCaml)

上一頁:集合和對映 索引 下一頁:可變資料
華夏公益教科書