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"
F# 有两个内置函数,fst
和 snd
,它们返回 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.
从 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 -> coords
。with
关键字创建 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