第三章 函数编程(四)

来源:互联网 发布:mtk平台驱动源码 编辑:程序博客网 时间:2024/05/21 15:41

第三章 函数编程(四)


定义类型

 

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# 写算法和处理数据应该已经有一个良好的感觉。下一章我们要讨论命令式编程,学习如何把函数式和命令式编程技术结合起来,共同处理任务,比如输入和输出。


0 0
原创粉丝点击