第三章 函数编程(四)
定义类型
F# 中提供了大量的功能用于自定义类型。所有 F#的类型定义可以分成两类:
一类是简单类型,称为记录(records),或元组(tuples),它是由几种类型形成复合类型(composite type,与 C 的结构或 C# 的类相似);
第二种类型是和类型(sum types),有时也称为联合类型(union types)。
元组(tuples)和记录(records)类型
元组是一种快速、方便组合值的方法。这些值用逗号分隔,可以用一个标识符引用,如下面例子的第一行;然后,通过切换[doing the reverse ]再取得这些值,如示例中的第二、三行完成,
逗号分隔的标识符在等号的左边,每一个标识符接收元组中的一个值。如果想忽略元组中的一个值,可以使用下划线(_)告诉编译器你对这个值不感兴趣,在第二、三行中就是这样的。
let pair = true, false
let b1, _ = pair
let _, b2 = pair
元组不同于 F# 中大多数用户定义的类型,因为不必要显式地用关键字type 去声明。定义类型,用关键字type,后面是类型名字,等号,然后是定义的类型。最简单的形式,可以用这个方法给已有的类型定义一个别名,包括元组。通常,给类型定义别名用处不大,但是给元组定义别名则非常有用,特别是当准备元组用于类型约束时。下面的例子演示了为类型和元组定义别名,以及如何用别名进行类型约束。
type Name = string
type Fullname = string * string
let fullNameToSting (x : Fullname) =
letfirst, second = x in
first + " " + second
在把组合多个类型组合成一个类型方面,记录类型与元组很相似,也有不同,记录类型中每个字段(field)都有名字。下面的例子演示定义记录类型的语法。
// define an organization with uniquefields
type Organization1 = { boss: string;lackeys: string list }
// create an instance of this organization
let rainbow =
{boss = "Jeffrey";
lackeys= ["Zippy"; "George"; "Bungle"] }
// define two organizations withoverlapping fields
type Organization2 = { chief: string;underlings: string list }
type Organization3 = { chief: string;indians: string list }
// create an instance of Organization2
let (thePlayers: Organization2) =
{chief = "Peter Quince";
underlings= ["Francis Flute"; "Robin Starveling";
"Tom Snout";"Snug"; "Nick Bottom"] }
// create an instance of Organization3
let (wayneManor: Organization3) =
{chief = "Batman";
indians= ["Robin"; "Alfred"] }
把字段的定义放在大括号中,用分号隔开;字段定义由字段名,冒号,字段类型组成;类型定义Organization1 是记录类型,字段名是唯一的。就是说,可以用简单的语法创建这个类型的一个实例,在创建时不需要用到类型的名字。创建记录方法,在大括号中放字段名,等号,字段值,如标识符Rainbow 所示:
F# 并不强制字段名唯一,因此,有时候编译器不能单独从字段名推断出字段类型;这样,编译器也就不能推断出领导的类型。要创建字段不唯一的记录,编译器必须静态知道所要创建记录的类型。如果编译器不能推断出记录的类型,就必须使用类型注释,如前一节中所描述的。类型 Organization2 和 Organization3 演示了类型注释的使用,thePlayers 和wayneManor 是它们的实例。可以看到,标识符的类型显式放在字段名的后面。
访问记录中的字段相当简单,其语法为:记录标识符名字,加上点,再加上字段名。下面的示例演示如何访问记录Organization 中的字段 chief。
// define an organization type
type Organization = { chief: string;indians: string list }
// create an instance of this type
let wayneManor =
{chief = "Batman";
indians = ["Robin"; "Alfred"] }
// access a field from this type
printfn "wayneManor.chief = %s" wayneManor.chief
默认情况下,记录是不可变的,对于命令式程序员来说,会觉得这样来说,记录就不是很有用,因为,改变字段几乎是不可避免的。出于这个目的,F# 提供了一个简单的语法,用修改后的字段创建记录的副本。创建记录副本的方法,在大括号中放记录的名字,后面是关键字 with,后面是一组需要修改的字段和修改后的值。这种方法的好处是不需要重新输入没有变化的字段。下面的示例演示这种方法,首先创建了wayneManor 的原始版本,然后,又创建了wayneManor‘,其中没有"Robin"。
// define an organization type
type Organization = { chief: string;indians: string list }
// create an instance of this type
let wayneManor =
{chief = "Batman";
indians = ["Robin"; "Alfred"] }
// create a modified instance of this type
let wayneManor‘ =
{wayneManor with indians = [ "Alfred" ] }
// print out the two organizations
printfn "wayneManor = %A"wayneManor
printfn "wayneManor‘ = %A"wayneManor‘
示例的运行结果如下:
wayneManor = {chief = "Batman";
indians = ["Robin";"Alfred"];}
wayneManor‘ = {chief = "Batman";
indians = ["Alfred"];}
访问记录中字段的另一种方法是使用模式匹配,就是说,可以用模式匹配去匹配记录类型中的字段。如你所想,用模式匹配去检查字段的语法和构造它的语法相似。可以用常量比较字段,字段 = 常量;可以用标识符给字段赋值,字段 = 标识符;也可以忽略一个字段,字段 = _。下面的例子中的函数 findDavid 就是用模式匹配访问记录中字段。
// type representing a couple
type Couple = { him : string ; her : string}
// list of couples
let couples =
[ {him = "Brad" ; her = "Angelina" };
{him = "Becks" ; her = "Posh" };
{him = "Chris" ; her = "Gwyneth" };
{him = "Michael" ; her = "Catherine" } ]
// function to find "David" froma list of couples
let rec findDavid l =
match l with
| {him = x ; her = "Posh" } :: tail -> x
| _:: tail -> findDavid tail
|[] -> failwith "Couldn‘t find David"
// print the results
printfn "%A" (findDavid couples)
函数 FindDavid 中的第一个规则,是做了实际工作,检查记录的her字段,看它的值是不是"Posh",David 的妻子。和标识符 x 关联的是him 字段,用于第二个规则的后半段。
示例的运行结果如下:
Becks
有一点十分重要,像这样针对记录进行模式匹配,只能使用文字值。因此,如果想让这个函数更通用一些,可以改变查找的对象,就需要在模式匹配中使用 when 子句:
let rec findPartner soughtHer l =
match l with
| {him = x ; her = her } :: tail when her = soughtHer -> x
| _:: tail -> findPartner soughtHer tail
|[] -> failwith "Couldn‘t find him"
字段值也可以是函数,因为这项技术主要用于连接可变状态,以形成类似于对象的值,我们会在第五章讨论。
联合(union)类型、和(sum)类型
联合类型,有时也称和类型,或差别联合(discriminated unions),它是将不同意义、结构的数据联合到一起的方法。
定义联合类型,如同所有的类型定义一样,使用关键字 type,加类型名,加等号,后面就是不同的构造器(constructors)定义,用竖线隔开;第一个竖线可以省略。
构造器的组成,名字必须以大写字母开头,是为了防止与标识符的名字相混淆,其后的关键字of是可选的;再后是组成构造器的类型。组成一个构造器的多个类型用星号隔开。类型中构造器的名字必须唯一。如果定义几个联合类型,它们构造器的名字也可以交叉。然而,这样做应该小心,因为在将来构造、使用联合类型时需要类型注释。
下面的例子定义一个类型Volume,它的值有三个不同的意义:liter(升)、USpint、imperial pint[ 都是我们不使用的计量单位,就不翻译了]。虽然数据结构相同,都是浮点数,但意义完全不同。在算法中混淆数据的意义通常会引起程序错误,从某种程度上讲,类型volume 就是为了避免这种错误。
type Volume =
|Liter of float
|UsPint of float
|ImperialPint of float
let vol1 = Liter 2.5
let vol2 = UsPint 2.5
let vol3 = ImperialPint (2.5)
构造联合类型的一个新实例的语法,构造器名,加这个类型的值;有多个值的,用逗号隔开;也可以把值放在括号中。用三个不同的构造器Volume 构造了三个不同的标识符 vol1、vol2 和 vol3。
把联合类型的值解构到它的基本部分,总是要用到模式匹配。针对联合类型的模式匹配,构造器组成模式匹配规则的前半部分。不一定要完整的规则;但是,如果规则不完整,就必须有一个默认规则,用标识符或通配符匹配所有剩余规则。构造器规则前面部分的组成,构造器名,加标识符或通配符去匹配其中的各种值。下面的函数convertVolumeToLiter、convertVolumeUsPint 和 convertVolumeImperialPint,演示了这个语法。
// type representing volumes
type Volume =
|Liter of float
|UsPint of float
|ImperialPint of float
// various kinds of volumes
let vol1 = Liter 2.5
let vol2 = UsPint 2.5
let vol3 = ImperialPint 2.5
// some functions to convert betweenvolumes
let convertVolumeToLiter x =
matchx with
|Liter x -> x
|UsPint x -> x * 0.473
|ImperialPint x -> x * 0.568
let convertVolumeUsPint x =
matchx with
|Liter x -> x * 2.113
|UsPint x -> x
|ImperialPint x -> x * 1.201
let convertVolumeImperialPint x =
matchx with
|Liter x -> x * 1.760
|UsPint x -> x * 0.833
|ImperialPint x -> x
// a function to print a volume
let printVolumes x =
printfn"Volume in liters = %f,
in us pints = %f,
in imperial pints = %f"
(convertVolumeToLiterx)
(convertVolumeUsPintx)
(convertVolumeImperialPintx)
// print the results
printVolumes vol1
printVolumes vol2
printVolumes vol3
示例的运行结果如下:
Volume in liters = 2.500000,
in us pints = 5.282500,
in imperial pints = 4.400000
Volume in liters = 1.182500,
in us pints = 2.500000,
in imperial pints = 2.082500
Volume in liters = 1.420000,
in us pints = 3.002500,
in imperial pints = 2.500000
解决这种问题的另一种办法是使用 F#(units of measure),在本章后面的“度量单位”一节讨论。
有类型参数的类型定义(Type Definitions with Type Parameters)
联合类型、记录类型都能参数化(parameterized)。类型参数化意思是保留这个类型定义中的一个或多个类型,以后由类型的使用者决定。这个概念与本章开始时讨论的可变类型相似,定义类型时,必须显式说明哪些类型是可变的。
F# 为类型参数化提供了两种语法。第一种,把要参数化的类型放在关键字 type 和类型的名字之间,如下所示:
type ‘a BinaryTree =
| BinaryNode of ‘a BinaryTree * ‘a BinaryTree
| BinaryValue of ‘a
let tree1 =
BinaryNode(
BinaryNode ( BinaryValue 1, BinaryValue 2),
BinaryNode ( BinaryValue 3, BinaryValue 4) )
第二种,把要参数化的类型放在尖括号之间,类型名字的后面。如下所示:
type Tree<‘a> =
| Node of Tree<‘a> list
| Value of ‘a
let tree2 =
Node( [ Node( [Value "one"; Value"two"] ) ;
Node( [Value "three"; Value"four"] ) ] )
像可变类型一样,类型参数的名字总是以单引号(‘)开头,后面是表示类型名字的字母、数字,通常只用一个字母。如果需要多个参数化的类型,用逗号隔开。以后,在整个类型定义期间都可以使用这个类型参数。前面的例子用了F# 提供的两种不同的语法,定义了两个参数化类型,
类型BinaryTree 使用的是OCaml 风格的语法,类型参数放在类型名字的前面;类型tree 使用了.NET 风格的语法,类型参数用尖括号放在类型名字的后面。
创建、使用参数化类型的实例,其语法与非参数化类型相同,这是因为编译器会自动推断参数化类型的类型参数。在下面的例子中,函数printBinaryTreeValues 和 printTreeValues 创建并使用 tree1、tree2:
// definition of a binary tree
type ‘a BinaryTree =
|BinaryNode of ‘a BinaryTree * ‘a BinaryTree
|BinaryValue of ‘a
// create an instance of a binary tree
let tree1 =
BinaryNode(
BinaryNode( BinaryValue 1, BinaryValue 2),
BinaryNode( BinaryValue 3, BinaryValue 4) )
// definition of a tree
type Tree<‘a> =
|Node of Tree<‘a> list
|Value of ‘a
// create an instance of a tree
let tree2 =
Node([ Node( [Value "one"; Value "two"] ) ;
Node([Value "three"; Value "four"] ) ] )
// function to print the binary tree
let rec printBinaryTreeValues x =
matchx with
|BinaryNode (node1, node2) ->
printBinaryTreeValuesnode1
printBinaryTreeValuesnode2
|BinaryValue x ->
printf"%A, " x
// function to print the tree
let rec printTreeValues x =
matchx with
|Node l -> List.iter printTreeValues l
|Value x ->
printf"%A, " x
// print the results
printBinaryTreeValues tree1
printfn ""
printTreeValues tree2
示例的运行结果如下:
1, 2, 3, 4,
"one", "two","three", "four",
你可能已经注意到了,虽然我们讨论了定义类型,创建类型的实例,查看实例,但并未讨论如何更新,更新这些类型是不可能的,这是因为值随时间变化违反了函数编程的思想。然而,F# 也提供了一些可以更新的类型,我们会在第四章中讨论。
递归类型定义(Recursive Type Definitions)
通常,类型的作用域是从它的声明开始,一直到声明它的这个源文件结束为止;如果有一个类型需要引用一个以后声明的类型,通常是做不到的,需要这样做的唯一原因是,这两个类型相互递归(mutually recursive)。
F# 有专门的语法定义相互递归的类型。这些类型必须,一起声明。在一块儿声明的类型必须彼此在一起声明,就是说,在它们之间不能有任何值的声明,在第一个类型定义之后,其他所有类型声明的关键字 type 用 and 替换。
以这种方式声明的类型不同任何常规方式声明的类型,它们可以引用这个块儿中任何其他的类型,甚至可以相互引用。
下面的示例演示了在 F# 中如何使用联合类型和记录类型表示 XML,在示例中,两个类型XmlElement 和 XmlTree 相互递归,在一块儿声明。如果它们单独声明,XmlElement 就不可能引用 XmlTree,因为XmlElement 在 XmlTree 之前声明;由于它们的声明用关键字 and 加在了一起,XmlElement 就有了一个类型为 XmlTree 的字段。
// represents an XML attribute
type XmlAttribute =
{AttribName: string;
AttribValue: string; }
// represents an XML element
type XmlElement =
{ElementName: string;
Attributes: list<XmlAttribute>;
InnerXml: XmlTree }
// represents an XML tree
and XmlTree =
|Element of XmlElement
|ElementList of list<XmlTree>
|Text of string
|Comment of string
|Empty
活动模式(Active Patterns)
活动模式提供了一种的方式使用 F# 的模式匹配,可以执行函数,看匹配是否发生,这就是为什么会称为活动(active)的原因。它设计的目的是能够在应用程序中更好地重用模式匹配逻辑。
所有的模式匹配都是有输入,然后用这些输入执行某种计算,决定匹配是否发生。有两种类型的活动模式:
完全活动模式(Complete active patterns),即,匹配可以分解成有限数量的情况;
散活动模式(Partial active patterns),即,既可能匹配,也可能失败。
首先,我们看一下完全活动模式。
完整的活动模式
定义活动模式的语法与定义函数相似,关键不同是表示活动模式的标识符用香蕉括号(banana brackets)括起来,它是由括号和竖线组成((| |))。放在香蕉括号中活动模式不同情况的名字用竖线分隔,活动模式的主体就是一个 F# 函数,必须返回在香蕉括号中给定的活动的每一种情况,每一种情况也可能返回额外的数据,就像联合类型一样。下面示例的第一部分演示了解析输入字符串的活动模式。
open System
// definitionof the active pattern
let(|Bool|Int|Float|String|) input =
// attempt to parse a bool
let success, res = Boolean.TryParse input
if success then Bool(res)
else
// attempt to parse an int
let success, res = Int32.TryParse input
if success then Int(res)
else
// attempt to parse a float (Double)
let success, res = Double.TryParse input
if success then Float(res)
else String(input)
//function to print the results by pattern
//matching over the active pattern
let printInputWithTypeinput =
match input with
| Bool b -> printfn "Boolean:%b" b
| Int i -> printfn "Integer:%i" i
| Float f -> printfn "Floatingpoint: %f" f
| String s -> printfn "String:%s" s
//print the results
printInputWithType"true"
printInputWithType"12"
printInputWithType"-12.1"
设计这个模式是用来确定输入的字符串是点面布尔型、整型、浮点型,或者字符串值,每一种情况的名字分别是Bool、Int、Float 和 String。示例依次使用由基本类库提供的方法 TryParse,确定输入值是否是布尔型、整型,或者浮点型;如果不是这几种类型,那么就是字符串;如果解析成功,则返回情况的名字连同解析的值。
在示例的第二部分,可以看到如何使用活动模式。活动模式可以把字符串看作就像是联合类型,可以匹配四种情况中的一种,并以强类型的方式由活动模式返回获得的数据。
下面是示例的运行结果:
Boolean: true
String: 12
Floating point: -12.100000
不完整的活动模式
定义不完整的活动模式的语法与完整的活动模式相似。不完整的活动模式只有一种情况,也放在香蕉括号中,如同完整的活动模式一样;不同的是,不完整的活动模式后面必须有一个竖线和一个下划线,说明它是不完整的(与完整的活动模式相对,只有一种情况)。
记住,完整与不完整的活动模式的关键不同是,完整的活动模式保证返回几种情况中的一种,而不管活动模式匹配成功与否;因此,不完整的活动模式是 option 类型,option 类型是简单的联合类型,它已经内置到 F# 的基本库,只有两种情况:Some 和 None。它的定义是这样的:
type option<‘a> =
| Some of ‘a
| None
这种类型,像它的名字所暗示的,用于表示一个值存在或者不存在。因此,不完整的活动模式既可以返回 Some,连同要返回的值一起,表示匹配;也可以返回 None,表示不匹配。
所有的活动匹配除了输入以处,可以有额外的参数;额外的参数放在活动模式的输入的前面。
下面的示例使用不完整的活动模式重新实现了前一个示例的内容,用 .NET 的正则表达式(regular expression)表示成功或失败。正则表达式是作为活动模式的参数给定的。
openSystem.Text.RegularExpressions
//the definition of the active pattern
let (|Regex|_|)regexPattern input =
// create and attempt to match a regular expression
let regex = new Regex(regexPattern)
let regexMatch = regex.Match(input)
// return either Some or None
if regexMatch.Success then
Some regexMatch.Value
else
None
//function to print the results by pattern
//matching over different instances of the
//active pattern
let printInputWithTypeinput =
match input with
| Regex "$true|false^" s-> printfn"Boolean:%s" s
| Regex @"$-?\d+^" s -> printfn "Integer: %s" s
| Regex "$-?\d+\.\d*^" s-> printfn"Floatingpoint: %s" s
| _ -> printfn "String: %s" input
//print the results
printInputWithType"true"
printInputWithType"12"
printInputWithType"-12.1"
因为完整的活动模式的行为与联合类型完全相同,就是说,如果有情况丢失,编译器会报错;而不完整的活动模式总是有一个兜底情况,就避免了编译错误。然而,不完整的活动模式的真正优势是可以把几个活动模式链接在一起,第一种匹配的情况将是被使用的。这可以从前面的示例中看到,它把三个正则表达式的活动模式在一起,每一种活动模式由不同的正则表达式模式参数化:一个匹配布尔输入,另一个匹配整型输入,第三个匹配浮点输入。
下面是示例的运行结果:
Boolean: true
String: 12
Floating point: -12.1
度量单位(Units of Measure)
度量单位是 F# 类型系统的一个有趣补充,它能够把数字值分类到不同的单位。它的思想是防止意外地错误使用数字值,例如,把表示英寸的值和表示的值在没有正确转换时就加起来。
为了定义度量单位,声明类型名,用属性 Measure 作为前缀。下面的示例创建了单位为米的类型(缩写 m):
[<Measure>]type m
默认情况下,度量单位使用浮点值,即,System.Double。要创建一个带单位的值,简单地在值的后面加上放在尖括号中的单位名。因此,使用下面的语法可以创建一个 meters 类型的值:
let meters =1.0<m>
现在,我们再重新审视一下“定义类型”一节中的示例,它是用联合类型来保证各种不同单位的类型不会混用,这个示例使用度量单位实现一些相似的功能。首先,分别为 liters 和 pints 定义不同的单位;然后,定义两个标识符表示不同的容积:一个用 pint 作单位,另一个用 liter 作单位;最后,尝试把这两个值加起来,这个操作应该会出错,因为在没有进行转换之前,不能把 pints 和 liters 加到一起。
[<Measure>]type liter
[<Measure>]type pint
let vol1 = 2.5<liter>
let vol2 = 2.5<pint>
let newVol = vol1 + vol2
运行程序,会产生下面的错误:
Program.fs(7,21): error FS0001: The unit ofmeasure ‘pint‘ does not match the unit of measure
‘liter‘
不同度量单位的加法和减法是不允许的,但是,除法和除法是可以的,它会产生一个新的度量单位。例如,我们知道,把 pint 转换成 liter,需要乘以一个liters 到pints 的比例系数,一个 liter 东倒西歪相当于 1.76 个 pints,因此,可以用下面的程序计算正确的转换比例:
let ratio = 1.0<liter> /1.76056338<pint>
标识符 ratio 的类型为float<liter/pint>,它使 liters 到 pints 的比例更清晰;而且,当一个类型为 float<pint> 的值乘以类型为 float<liter/pint>的值,结果的类型自动是 float<liter>,正如我们所希望的一样。这样,我们现在就能用下面的程序保证 pints 在相加之前,案例地转换成 liters:
// define some units of measure
[<Measure>]type liter
[<Measure>]type pint
// define some volumes
let vol1 = 2.5<liter>
let vol2 = 2.5<pint>
// define the ratio of pints to liters
let ratio = 1.0<liter> /1.76056338<pint>
// a function to convert pints to liters
let convertPintToLiter pints =
pints * ratio
// perform the conversion and add thevalues
let newVol = vol1 + (convertPintToLitervol2)
异常与异常处理(Exceptions and Exception Handling)
在 F# 中,定义异常的语法与定义联合类型的构造器相似,而处理异常的语法与模式匹配相似。
定义异常,使用关键字 exception,加异常的名字,然后是关键字 of 和异常可能包含值的类型,有多个类型的用星号隔开,这一项是可选的。下面的示例定义了一个异常WrongSecond,它包含了一个整数。
exception WrongSecond of int
可以用关键字 raise 引发异常,就像下面函数 testSecond 中 else 子句所显示的;F# 还有另一个引发异常的关键字failwith 函数,如下面的 if 子句。如果这是一个很普通的情况,你引发的异常只想描述一下发生了什么错误,就可以用failwith 引发一个一般异常,它包含了你传递给这个函数的文本。
// define an exception type
exception WrongSecond of int
// list of prime numbers
let primes =
[2; 3; 5; 7; 11; 13; 17; 19; 23; 29; 31; 37; 41; 43; 47; 53; 59 ]
// function to test if current second isprime
let testSecond() =
try
let currentSecond = System.DateTime.Now.Second in
// test if current second is in the list of primes
if List.exists (fun x -> x = currentSecond) primes then
// use the failwith function to raise an exception
failwith "A prime second"
else
// raise the WrongSecond exception
raise (WrongSecond currentSecond)
with
//catch the wrong second exception
WrongSecond x ->
printf "The current was %i, which is not prime" x
// call the function
testSecond()
如 testSecond 所显示的,处理异常使用关键字 try 和 with,在 try 和 with 之间的是需要进行错误处理的原因的表达式;在 with 之后必须有一个或多个模式匹配的规则。当尝试匹配 F# 异常时,语法与匹配联合类型的构造器一样。前半部分的规则包含异常的名字,加标识符或者通配符,匹配异常包含的值;后半部分的规则是一个表达式,描述应该如何处理异常。它与常规的模式匹配构造之间的主要不同在于,如果模式匹配不完整,不会发出错误或警告,这是因为任何未经处理的异常会继续传播,直到到达顶层,并停止运行。这个示例处理了异常wrongSecond,而让由failwith 引发的异常继续传播。
F# 还提供了关键字 finally,与关键字 try 相对应,不能与关键字with 相连。不管异常是否发生,finally 表达式都会执行。下面的示例演示了用 finally 语句保证写文件结束后,被关闭并释放:
// function to write to a file
let writeToFile() =
//open a file
letfile = System.IO.File.CreateText("test.txt")
try
// write to it
file.WriteLine("Hello F# users")
finally
// close the file, this will happen even if
// an exception occurs writing to the file
file.Dispose()
// call the function
writeToFile()
警告
有OCaml 背景的程序员在使用地# 中的异常时要小心,由于通用语言运行时体系的原因,引发异常的代价是相当昂贵的,与 OCaml 相比,高出不少。因此,如果你打算引发许多异常,应该仔细评估你的代码,决定性能的代价是否值得;如果代价太高,最好适当地修改代码。
延迟计算(Lazy Evaluation)
延迟计算是与函数编程紧密结合的,其理论是这样的,如果在语言中没有副作用,编译器或运行时可以自由选择表达式的计算顺序。
如你所知,F# 的函数是可以有副作用的,因此,编译器或运行时想在函数计算上不受约束是不可能的;所以说,F# 必须有严格的计算顺序,或者称为严格语言(strict language)。不过,我们仍然可以利用延迟计算,只明必须明确哪些计算必须延迟计算,即,以延迟方式计算。
延迟计算使用关键字 lazy。在延迟表达式中的计算直到显式强制计算时才进行,使用 Lazy 模块中的 force 函数。当针对特定的延迟表达式应用 force 函数时,值才计算,结果被缓存起来;以后对这个 force 函数的调用,直接返回缓存的结果,不管什么时候,即使是引发异常。
下面的代码演示了延迟计算的简单应用:
let lazyValue = lazy ( 2 + 2 )
let actualValue = Lazy.force lazyValue
printfn "%i" actualValue
第一行简单地延迟表达式留着以后计算;第二行强制计算;最后,打印结果。
延迟计算的值已经被缓存起来了,因此,所有计算这个值时所发生的任何副作用,都只会在第一次强制计算时发生。这是很容易演示的,看下面的示例。
let lazySideEffect =
lazy
( let temp = 2 + 2
printfn "%i"temp
temp )
printfn "Force value the first time: "
let actualValue1 = Lazy.force lazySideEffect
printfn "Force value the second time: "
let actualValue2 = Lazy.force lazySideEffect
在这个示例中,有一个延迟值在计算时会有副作用:值会写到控制台。为了演示这个副作用只会发生一次,我们强制这个值计算两次。正如你从结果中所看到的,写到台只发生一次:
Force value the first time:
4
Force value the second time:
延迟计算在处理集合时也是有用的。延迟集合的思想是这样的:集合中的元素只在需要时才计算,一些集合也可以缓存这些计算,因此,不需要重新计算元素。F# 编程中,最常用的延迟计算的集合是 seq 类型,它是 BCL IEnumerable 类型的简写。创建和操作 seq 值,使用 Seq 模块中的函数。还有其他许多值也与 seq 类型兼容,例如,所有 F# 的列表和数组,以及 F# 库函数和 .NET BCL 中的大多数集合类型都与 seq 兼容。
创建延迟集合最重要的函数,也是最难于理解的,可能就是 unfold 了。这个函数可以创建延迟列表,它的难点在于,你必须提供一个函数,用于对列表中的所有元素进行重复计算。传递给 Seq.unfold 的这个函数的可以是任意类型,返回的结果是可选(option)类型。可选类型是联合类型,可能是 None,也可能是 Some(x),这里的 x 是任意类型的值。None 列表的结尾;Some 构造器必须包含一个元组,元组中的第一个项表示将会成为这个列表中第一个值的值;元组中的第二项是将会传递到函数,用于下一次调用的值,可以把这个值看作是一个累加器。
下面的示例演示这个函数的工作原理。标识符 lazyList 将包含三个值.如果传递给函数的值小于13,它将把这个值追加到列表,形成列表的元素;然后,这个值再加1,传递给列表;这将成为传递给下次被调用的函数的值。如果这个值大于或等于 13,通过返回 None,终止列表。
// the lazy list definition
let lazyList =
Seq.unfold
(fun x ->
if x < 13 then
// if smaller than the limit return
// the current and next value
Some(x, x + 1)
else
// if great than the limit
// terminate the sequence
None)
10
// print the results
printfn "%A" lazyList
示例运行结果如下:
10
11
12
序列可以用来表示无穷列表,无穷列表不能用传统的列表表示,因为它受可用内存数量的限制。下面的示例通过创建 fibs 进行演示,斐波那契数的就是一个无穷列表。为了方便看到结果,示例使用了 Seq.take 函数,把序列的前 20 项转换成列表,但是,实际计算的斐波那契数远远不止这些,因为使用了 F# 的 bigint 整数,因此,不受 32 位整数的限制。
// create an infinite list of Fibonacci numbers
let fibs =
Seq.unfold
(fun (n0, n1) ->
Some(n0, (n1, n0 + n1)))
(1I,1I)
// take the first twenty items from the list
let first20 = Seq.take 20 fibs
// print the finite list
printfn "%A" first20
示例的运行结果如下:
[1I; 1I; 2I; 3I; 5I; 8I; 13I; 21I; 34I; 55I; 89I; 144I; 233I; 377I;610I; 987I;
1597I; 2584I; 4181I; 6765I]
注意,这两个序列也可以用本章介绍的列表推导进行创建,如果列表推导基于序列,那么,它自动就是延迟的。
第三章小结
在这一章,我们学习了 F#中主要的函数编程构造,它是语言的核心,至此,我们对如何用 F# 写算法和处理数据应该已经有一个良好的感觉。下一章我们要讨论命令式编程,学习如何把函数式和命令式编程技术结合起来,共同处理任务,比如输入和输出。
原文:http://blog.csdn.net/hadstj/article/details/21633885