跳转到内容

F# 编程/元组和记录

来自维基教科书,面向开放世界的开放书籍
前一个:选项类型 索引 下一个:列表
F# : 元组和记录

定义元组

[编辑 | 编辑源代码]

元组被定义为一个逗号分隔的值集合。例如,(10, "hello") 是一个 2 元组,类型为 (int * string)。元组对于创建将相关值组合在一起的临时数据结构非常有用。请注意,括号不是元组的一部分,但通常需要添加它们以确保元组只包含你认为它包含的内容。

let average (a, b) =
    (a + b) / 2.0

此函数的类型为 float * float -> float,它接收一个 float * float 元组并返回另一个 float

> let average (a, b) =
    let sum = a + b
    sum / 2.0;;

val average : float * float -> float

> average (10.0, 20.0);;
val it : float = 15.0

注意,元组被视为单个参数。因此,元组可用于返回多个值

示例 1 - 一个函数,它将一个 3 元组乘以一个标量值以返回另一个 3 元组。

> let scalarMultiply (s : float) (a, b, c) = (a * s, b * s, c * s);;

val scalarMultiply : float -> float * float * float -> float * float * float

> scalarMultiply 5.0 (6.0, 10.0, 20.0);;
val it : float * float * float = (30.0, 50.0, 100.0)

示例 2 - 一个函数,它反转传递给函数的任何内容的输入。

> let swap (a, b) = (b, a);;
val swap : 'a * 'b -> 'b * 'a

> swap ("Web", 2.0);;
val it : float * string = (2.0, "Web")

> swap (20, 30);;
val it : int * int = (30, 20)

示例 3 - 一个函数,它除两个数并同时返回余数。

> let divrem x y =
    match y with
    | 0 -> None
    | _ -> Some(x / y, x % y);;

val divrem : int -> int -> (int * int) option

> divrem 100 20;; (* 100 / 20 = 5 remainder 0 *)
val it : (int * int) option = Some (5, 0)

> divrem 6 4;; (* 6 / 4 = 1 remainder 2 *)
val it : (int * int) option = Some (1, 2)

> divrem 7 0;; (* 7 / 0 throws a DivisionByZero exception *)
val it : (int * int) option = None

每个元组都有一个名为 arity 的属性,它表示用于定义元组的参数数量。例如,一个 int * string 元组由两部分组成,因此它具有 2 的元数,一个 string * string * float 具有 3 的元数,依此类推。

元组模式匹配

[编辑 | 编辑源代码]

元组上的模式匹配很简单,因为用于声明元组类型的语法也用于匹配元组。

示例 1

假设我们有一个函数 greeting,它根据指定的名字和/或语言打印出自定义问候语。

let greeting (name, language) =
    match (name, language) with
    | ("Steve", _) -> "Howdy, Steve"
    | (name, "English") -> "Hello, " + name
    | (name, _) when language.StartsWith("Span") -> "Hola, " + name
    | (_, "French") -> "Bonjour!"
    | _ -> "DOES NOT COMPUTE"

此函数的类型为 string * string -> string,这意味着它接收一个 2 元组并返回一个字符串。我们可以在 fsi 中测试此函数

> greeting ("Steve", "English");;
val it : string = "Howdy, Steve"
> greeting ("Pierre", "French");;
val it : string = "Bonjour!"
> greeting ("Maria", "Spanish");;
val it : string = "Hola, Maria"
> greeting ("Rocko", "Esperanto");;
val it : string = "DOES NOT COMPUTE"

示例 2

我们可以使用替代模式匹配语法方便地匹配元组的形状

> let getLocation = function
    | (0, 0) -> "origin"
    | (0, y) -> "on the y-axis at y=" + y.ToString()
    | (x, 0) -> "on the x-axis at x=" + x.ToString()
    | (x, y) -> "at x=" + x.ToString() + ", y=" + y.ToString() ;;

val getLocation : int * int -> string

> getLocation (0, 0);;
val it : string = "origin"
> getLocation (0, -1);;
val it : string = "on the y-axis at y=-1"
> getLocation (5, -10);;
val it : string = "at x=5, y=-10"
> getLocation (7, 0);;
val it : string = "on the x-axis at x=7"

fstsnd

[编辑 | 编辑源代码]

F# 有两个内置函数,fstsnd,它们返回 2 元组中的第一个和第二个项目。这些函数定义如下

let fst (a, b) = a
let snd (a, b) = b

它们具有以下类型

val fst : 'a * 'b -> 'a
val snd : 'a * 'b -> 'b

以下是在 FSI 中的几个示例

> fst (1, 10);;
val it : int = 1
> snd (1, 10);;
val it : int = 10
> fst ("hello", "world");;
val it : string = "hello"
> snd ("hello", "world");;
val it : string = "world"
> fst ("Web", 2.0);;
val it : string = "Web"
> snd (50, 100);;
val it : int = 100

同时分配多个变量

[编辑 | 编辑源代码]

元组可用于同时分配多个值。这与 Python 中的元组解包相同。执行此操作的语法为

let val1, val2, ... valN = (expr1, expr2, ... exprN)

换句话说,你将一个逗号分隔的N个值列表分配给一个N元组。以下是在 FSI 中的示例

> let x, y = (1, 2);;

val y : int
val x : int

> x;;
val it : int = 1

> y;;
val it : int = 2

分配的值数量必须与函数返回的元组的元数匹配,否则 F# 将引发异常

> let x, y = (1, 2, 3);;

  let x, y = (1, 2, 3);;
  ------------^^^^^^^^

stdin(18,13): error FS0001: Type mismatch. Expecting a
	'a * 'b
but given a
	'a * 'b * 'c.
The tuples have differing lengths of 2 and 3.

元组和 .NET 框架

[编辑 | 编辑源代码]

从 F# 的角度来看,.NET 基本类库中的所有方法都接收一个参数,该参数是具有不同类型和元数的元组。例如

C# 函数签名 F# 函数签名
System.String String Join(String separator, String[] value) val Join : (string * string array) -> string
System.Net.WebClient void DownloadFile(String uri, String fileName) val DownloadFile : (string * string) -> unit
System.Convert String ToString(int value, int toBase) val ToString : (int * int) -> string
System.Math int DivRem(int a, int b, out int remainder) val DivRem : (int * int) -> (int * int)
System.Int32 bool TryParse(String value, out int result) val TryParse : string -> (bool * int)

一些方法,例如上面显示的 System.Math.DivRem 以及其他方法,例如 System.Int32.TryParse 通过输出变量返回多个值。F# 允许程序员省略输出变量;使用此调用约定,F# 将以元组的形式返回函数的结果,例如

> System.Int32.TryParse("3");;
val it : bool * int = (true, 3)

> System.Math.DivRem(10, 7);;
val it : int * int = (1, 3)

定义记录

[编辑 | 编辑源代码]

记录类似于元组,只是它包含命名字段。使用以下语法定义记录

type recordName =
    { [ fieldName : dataType ] + }
+ 表示元素必须出现一次或多次。

以下是一个简单的记录

type website =
    { Title : string;
        Url : string }

与元组不同,记录使用 type 关键字显式定义为它自己的类型,记录字段定义为用分号分隔的列表。(在许多方面,记录可以被认为是一个简单的。)

通过指定记录的字段来创建 website 记录,如下所示

> let homepage = { Title = "Google"; Url = "http://www.google.com" };;
val homepage : website

请注意,F# 通过字段的名称和类型来确定记录的类型,而不是使用字段的顺序。例如,虽然上面的记录是使用 Title 首先和 Url 最后定义的,但以下写法完全合法

> { Url = "http://www.microsoft.com/"; Title = "Microsoft Corporation" };;
val it : website = {Title = "Microsoft Corporation";
                    Url = "http://www.microsoft.com/";}

使用点表示法很容易访问记录的属性

> let homepage = { Title = "Wikibooks"; Url = "http://www.wikibooks.org/" };;

val homepage : website

> homepage.Title;;
val it : string = "Wikibooks"

> homepage.Url;;
val it : string = "http://www.wikibooks.org/"

克隆记录

[编辑 | 编辑源代码]

记录是不可变类型,这意味着不能修改记录的实例。但是,可以使用克隆语法方便地克隆记录

type coords = { X : float; Y : float }

let setX item newX =
    { item with X = newX }

方法 setX 的类型为 coords -> float -> coordswith 关键字创建 item 的一个副本,并将它的 X 属性设置为 newX

> let start = { X = 1.0; Y = 2.0 };;
val start : coords

> let finish = setX start 15.5;;
val finish : coords

> start;;
val it : coords = {X = 1.0;
                   Y = 2.0;}
> finish;;
val it : coords = {X = 15.5;
                   Y = 2.0;}

请注意,setX 创建了记录的副本,它实际上并没有改变原始记录实例。

以下是一个更完整的程序

type TransactionItem =
    { Name : string;
        ID : int;
        ProcessedText : string;
        IsProcessed : bool }

let getItem name id =
    { Name = name; ID = id; ProcessedText = null; IsProcessed = false }

let processItem item =
    { item with
        ProcessedText = "Done";
        IsProcessed = true }
    
let printItem msg item =
    printfn "%s: %A" msg item

let main() =
    let preProcessedItem = getItem "Steve" 5
    let postProcessedItem = processItem preProcessedItem

    printItem "preProcessed" preProcessedItem
    printItem "postProcessed" postProcessedItem
    
main()

此程序处理 TransactionItem 类的实例并打印结果。此程序输出以下内容

preProcessed: {Name = "Steve";
 ID = 5;
 ProcessedText = null;
 IsProcessed = false;}
postProcessed: {Name = "Steve";
 ID = 5;
 ProcessedText = "Done";
 IsProcessed = true;}

记录模式匹配

[编辑 | 编辑源代码]

我们可以像匹配元组一样轻松地匹配记录

open System

type coords = { X : float; Y : float }
 
let getQuadrant = function
    | { X = 0.0; Y = 0.0 } -> "Origin"
    | item when item.X >= 0.0 && item.Y >= 0.0 -> "I"
    | item when item.X <= 0.0 && item.Y >= 0.0 -> "II"
    | item when item.X <= 0.0 && item.Y <= 0.0 -> "III"
    | item when item.X >= 0.0 && item.Y <= 0.0 -> "IV"
 
let testCoords (x, y) =
    let item = { X = x; Y = y }
    printfn "(%f, %f) is in quadrant %s" x y (getQuadrant item)
 
let main() =
    testCoords(0.0, 0.0)
    testCoords(1.0, 1.0)
    testCoords(-1.0, 1.0)
    testCoords(-1.0, -1.0)
    testCoords(1.0, -1.0)
    Console.ReadKey(true) |> ignore
 
main()

请注意,模式情况使用与创建记录相同的语法定义(如第一个情况所示),或者使用保护条件(如其余情况所示)。不幸的是,程序员不能在模式情况中使用克隆语法,因此像 | { item with X = 0 } -> "y-axis" 这样的情况将无法编译。

上面的程序输出

(0.000000, 0.000000) is in quadrant Origin
(1.000000, 1.000000) is in quadrant I
(-1.000000, 1.000000) is in quadrant II
(-1.000000, -1.000000) is in quadrant III
(1.000000, -1.000000) is in quadrant IV
前一个:选项类型 索引 下一个:列表
华夏公益教科书