跳转到内容

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)

上一页:集合和映射 索引 下一页:可变数据
华夏公益教科书