第四章 命令编程(一)

来源:互联网 发布:mac口红吧 编辑:程序博客网 时间:2024/06/06 02:45

第四章 命令编程(一)

 

正如在第三章中所见,我们可以用F# 进行纯函数编程,然而,有些问题,最明显的I/O,没有几种状态改变,几乎无法处理。F# 并不要求一定要用无状态方式编程,既可以使用可变的标识符,其值随时间而变化;也可以用其他构造,以支持命令编程。我们在第三章中已经看到了一些。所有输出到控制台的例子,在函数代码之外,多少都有几行命令代码。在这一章,我们将浏览这些构造,当然还有其他许多,更详细地介绍。

首先,我们将学习空(unit)类型,这是一个特别的类型,意思是“没有值”,它开启了一些命令编程方式;接下来,还要学习一些F# 管理可变状态(mutable state)的方法或类型,它们的值随时间而变化,包括可变标识符、引用(ref)类型、可变记录类型和数组;最后,学习使用.NET 库函数,这个主题包括调用静态方法、创建对象及处理其成员、使用特殊成员,比如索引器和事件,使用F# 的向前管道(|>)运算符,它在用.NET 库函数时是很方便的。

 

 

空(unit)类型

 

任何不接受值,或者不返回值的函数,它的类型就是空,它与C# 中的void、CLR 中的System.Void 类型相似。对于函数式程程序员来说,函数不接受值,或不返回值,好像没有什么意义,因为函数不接受值,或不返回值,那它就什么也做不了。在命令范式中,我们知道,函数有副作用存在,即使它不接受值,或不返回值,仍然有它的用途。空类型是文字表述,或写作一对括号(()),就是说,不论什么时候,想让函数不接受值,或不返回值,就在代码中放一对括号。

 

let aFunction() =

  ()

 

在这个示例中,aFunction 是一个函数,因为把括号放在标识符的后面,这是放参数的地方;如果你不这样做,那么,就可能表示aFunction 不是函数,而仅是一个值,不是函数的值。我们知道,所有的函数都是值,但在这里,函数和非函数值的区别是非常重要的。如果aFunction 是一个非函数的值,其中的表达式只计算一次;而由于它是函数,因此,表达式在每次调用时都会计算。

类似地,在等号后放括号,告诉编译器不返回值。通常,需要在等号和括号之间放一些内容,否则,函数就没有意义了。出于使示例简单化的目的,就保持这个函数无意义了。现在,将看到aFunction 的类型,最容易的办法是,在 visual studio 中使用工具提示,或者使用 F# 交互进行编译;还有一个办法,用编译器的fsc -i 开关,结果如下:

 

val aFunction: unit -> unit

 

如你所见,aFunction 的类型是函数,接收空类型的参数,转换到空类型的值。因为现在编译器知道这个不返回任何值,我们就可以把它用于一些特别的命令式构造。调用这个函数,可以使用关键字 let,加一对括号和等号。关键字 let 的这种特别用法,表示“调用一个不返回值的函数”;还可以使用关键字 do,或者简单地调用这个函数,根本不需要任何额外的关键字,把这个函数放在放在顶层:

 

let aFunction() =

()

let () = aFunction ()

// -- or --

do aFunction ()

// -- or --

aFunction ()

 

类似地,可以把几个返回空的函数链接起来,放一个函数中,只要保证它们有相同的缩进就行了。下面的示例中几个printfn 函数链接到一起,输出文本打控制台:

 

let poem() =

  printfn "I wandered lonely as acloud"

  printfn "That floats on high o'er valesand hills,"

  printfn "When all at once I saw acrowd,"

  printfn "A host, of goldendaffodils"

poem()

 

只能以这种方式,使用返回空类型的函数,这种认识并不恰当。然而,使用非空类型的函数,会产生警告,而这是大部分程序员都力求避免的。因此,为了避免警告,有时需要把返回值的函数转换成空类型,通常是因为这个函数不仅返回值,而且有副作用。只使用用 F# 写的  F# 库函数相当罕见(虽然这种情况也存在),而更多的情况是使用不是用F# 写的.NET 库函数。

下面的例子演示如何丢弃函数的返回值,使函数的返回结果为空:

 

let getShorty() = "shorty"

let _ = getShorty()

// -- or --

ignore(getShorty())

// -- or --

getShorty() |> ignore

 

首先,定义一个返回字符串的函数getShorty。现在,想像一下,由于某种原因,你想调用这个函数,并忽略结果。接下来的两行代码演示了两种不同的方法:一、用let 表达式,在标识符的位置用下划线,这就告诉编译器,我们对这个值不感兴趣;二、这里很常规的做法,把它用函数ignore 包起来,这个函数有F# 的基本库中,看第三行的演示。最后一行演示了调用ignore 的另一种方法,用向前传递运算符[ pass-forward operator,也就是 pipe-forwardoperator,向前管道运算符,|>),把 getShorty() 的结果传递给ignore 函数。向前传递运算符的有关内容参见第三章。

 

 

关键字 mutable(可变)

 

在第三章,我们讨论了如何用 let 将标识符绑定到值,如何在某种情况下,可以重新定义、重新绑定标识符,但不能修改。如果想让定义标识符的值随时间而变化,可用关键字 mutable。一个专门的运算符,向左的 ASCII 箭头,或直接称左箭头(left ASCII arrow,leftarrow),由小于号和破折号组成(<-),用它来更新标识符。更新操作使用左箭头,其类型为空,因此,可以将这类操作链接到一起,我们在前一节讨论过。下面的例子演示了定义一个字符串类型的可变标识符,然后改变它的值:

 

// amutable idendifier

let mutable phrase ="How can I besure, "

//print the phrase

printfn"%s" phrase

//update the phrase

phrase<- "Ina world that's constantly changing"

//reprint the phrase

printfn"%s" phrase

 

运行结果如下:

 

How can I be sure,

In a world that's constantly changing

 

乍一看,这与重新定义标识符没有什么不同,但实际上是有关键的不同。当使用左箭头更新可变标识符,只能改变值,但不能改变类型;而重新定义标识符,两者都能改变。下面的示例,如果要改变类型,编译器会报错 [ 实际上,在 visual studio 中,根本不需要编译就能看到错误,在 1 的下面会出现红色的波浪线;当鼠标指向 1 时,出现的错误同下。]:

 

let mutable number ="one"

number<- 1

 

编译时,会报下面的错误:

 

Prog.fs(9,10): error: FS0001: Thisexpression has type

int

but is here used with type

string

[

errorFS0001: 此表达式应具有类型

    string   

而此处具有类型

    int   

]

另一个主要不同是这些改变是可见的。而重新定义标识符,改变只在新标识符的作用域内可见;当它离开这个作用域,就恢复原来值。这与使用可变标识符不同,其改变是永久的,不论在什么作用域中。如下面的例子所示:

 

//demonstration of redefining X

let redefineX() =

  let x = "One"

  printfn "Redefining:\r\nx= %s" x

  if truethen

    let x = "Two"

    printfn "x = %s" x

  printfn "x = %s" x

 

//demonstration of mutating X

let mutableX() =

  let mutable x ="One"

  printfn "Mutating:\r\nx= %s" x

  if truethen

    x <- "Two"

    printfn "x = %s" x

  printfn "x = %s" x

 

//run the demos

redefineX()

mutableX()

 

 

运行结果如下:

 

Redefining:

x = One

x = Two

x = One

Mutating:

x = One

x = Two

x = Two

 

定义为可变的标识符有些限制,不能用在子函数中。看下一个例子:

 

let mutableY() =

  let mutable y ="One"

  printfn "Mutating:\r\nx= %s" y

  let f() =

    // this causes an error as

    // mutables can't be captured

    y <- "Two"

    printfn "x = %s" y

  f()

  printfn "x = %s" y

 

运行是会报错:

 

Prog.fs(35,16): error: The mutable variable'y' has escaped its scope. Mutable

variables may not be used within an innersubroutine. You may need to use a heapallocated

mutable reference cell instead, see 'ref'and '!'.

[

errorFS0407: 可变变量“y”的使用方式无效。无法由闭包来捕获可变变量。请考虑取消此变量使用方式,或通过“ref”和“!”使用堆分配的可变引用单元格。

]

如错误提示所说,这就是为会什么要用引用(ref)类型,一种特别的可变记录,它提供了管理需要在几个函数之间共享的可变变量。我们在下一节会讨论记录,再下一节讨论引用类型。

 

 

定义可变记录(Mutable Record)类型

 

在第三章,我们首次看到过记录类型,讨论了如何更新记录的字段。这是因为记录类型通常是不可变的;F# 提供了专门的语法更新记录类型中的字段,在记录类型的字段前加关键字mutable。需要强调的是,这种操作改变了记录的字段内容,而不是改变记录本身。

 

// arecord with a mutable field

type Couple = { Her:string; mutable Him: string }

 

// acreate an instance of the record

let theCouple = { Her ="ElizabethTaylor ";Him ="NickyHilton" }

 

//function to change the contents of

//the record over time

let changeCouple() =

  printfn "%A" theCouple

  theCouple.Him <- "MichaelWilding"

  printfn "%A" theCouple

  theCouple.Him <- "MichaelTodd"

  printfn "%A" theCouple

  theCouple.Him <- "EddieFisher"

  printfn "%A" theCouple

  theCouple.Him <- "RichardBurton"

  printfn "%A" theCouple

  theCouple.Him <- "RichardBurton"

  printfn "%A" theCouple

  theCouple.Him <- "JohnWarner"

  printfn "%A" theCouple

  theCouple.Him <- "LarryFortensky"

  printfn "%A" theCouple

 

//call the fucntion

changeCouple()

 

运行结果如下:

 

{her = "Elizabeth Taylor "; him ="Nicky Hilton"}

{her = "Elizabeth Taylor "; him ="Michael Wilding"}

{her = "Elizabeth Taylor "; him ="Michael Todd"}

{her = "Elizabeth Taylor "; him ="Eddie Fisher"}

{her = "Elizabeth Taylor "; him ="Richard Burton"}

{her = "Elizabeth Taylor "; him ="Richard Burton"}

{her = "Elizabeth Taylor "; him ="John Warner"}

{her = "Elizabeth Taylor "; him ="Larry Fortensky"}

 

这个例子就使用了mutable记录。定义了类型 couple,其中字段 him 是可变的,而字段 her 不可变。接着,初始化 couple 的一个实例,然后,多次改变 him 值,并同时显示每次改变的结果。应该注意到,关键字 mutable [ 没有 ] 应用到每一个字段,因此,任何企图改变不是可变的字段,将会在编译时报错。看下面例子的第二行:

 

theCouple.Her<- "SybilWilliams"

printfn"%A" theCouple

 

企图编译时,报错:

 

prog.fs(2,4): error: FS0005: This field isnot mutable

 

 

引用(ref)类型

 

程序使用可变状态,即,值可以随时改变,引用类型是一种简单方法。引用类型是 F# 库函数中定义的、唯一的、有可变字段的记录类型。有些运算符的定义要使访问、更新字段尽可能的简单,引用类型的 F# 定义使用类型参数化(type parameterization),其概念在前一章中作过介绍。这样,尽管引用类型的值可以是任意类型,但是,一旦已经创建了这个值的实例,其值的类型就不能改变。

创建引用类型的实例很简单,使用关键字 ref,后面加上表示引用类型的值。下面示例显示了编译器输出(使用 -I 选项,可以看出 phrase 的类型是 string ref,即,只能包含字符串的引用类型)。

 

let phrase = ref "Inconsistency"

 

val phrase : string ref = {contents ="Inconsistency";}

 

这个语法与联合类型的构造器相似,我们在前一章介绍过。引用类型有两个内置的运算符用于访问引用类型:感叹号(!)用于访问引用类型的值;由冒号加等号组合的运算符(:=)用于更新。运算符 ! 总是返回匹配这个引用类型内容的类型的值,由于类型参数化,编译器能够知道;运算符 := 的类型为空,因为它没有返回。

下面的例子演示使用引用类型计算数组内容的总和。函数 totalArrary 的第三行,创建引用类型,这里,被初始化为 0;第七 [ 是最后一行吗? ] 行,在数组定义后是 let 绑定,可以看到访问、更新引用类型。首先,! 用于访问引用类型的值,然后,把它与数组中的当前值相加,这个引用类型的值用:= 运算符更新。现在,代码将输出6 到控制台:

 

let totalArray () =

  //define an array literal

  letarray = [| 1; 2; 3 |]

  //define a counter

  lettotal = ref 0

  //loop over the array

  forx in array do

    //kep a running total

    total:= !total + x

  //print the total

  printfn"total: %i" !total

 

totalArray()

 

运行结果如下:

 

6

 

警告:如果你过去经常使用C 家族的编程语言,那么就要小心了。阅读F# 代码,很容易混淆引用类型的运算符! 与逻辑非运算符。逻辑非操作在F# 中使用函数调用not。

 

引用类型可以在几个函数之间共享可变值。一个标识符可以绑定到引用类型,它定义在所有想使用这个值的函数所共有作用域;然后,所有的函数就可以按它们自己的方式使用标识符的值,改变或者仅仅读取。在F# 中,因为函数可以像值一样传递给函数,而这个值可以随函数到任何地方。这个过程称为捕获本地(capturing a local)或者创建闭包(creating a closure)。

下面的例子,通过定义三个函数inc、dec、show 来演示共享引用类型中的整数。函数inc、dec、show 都在它们自己的私有作用域中定义的,最后在顶层返回一个元组,因此,它们在任何地方都是可见的。注意,n 并不返回,它保持私有,但函数inc、dec、show 都可以访问n。这对于控制哪些运算符可以发生在可变数据是非常有用的。

 

// capute the inc, dec and show funtions

let inc, dec, show =

  //define the shared state

  letn = ref 0

  //a function to increment

  letinc () =

    n:= !n + 1

  //a function to decrement

  letdec () =

    n:= !n - 1

  //a function to show the current state

  letshow () =

    printfn"%i" !n

  //return the functions to the top level

  inc,dec, show

 

// test the functions

inc()

inc()

dec()

show()

 

运行结果如下:

 

1

 

 

数组(arrays)

 

数组的概念大多数程序员都很熟悉,因为,几乎所有的语言都有某种数组类型。F# 的数组类型是基于BCL 的System.Array 类型,因此,凡是以前用过C# 或VB 中数组的,都会发现其基本概念是相同的。

F# 中的数组是可变的集合类型;我们把数组与第三章中讨论的不可变类型列表相比较,更利于掌握。数组和列表都是集合,但是,数组也一些属性与列表完全不同。数组中的值是可更新的,而列表不行;列表可以动态扩展,而数组不行。一维数组有时也称为向量(vectors),多维数组也称为矩阵(matrices)。数组是由用分号隔开的系列组成的,分别用括号和竖线([|和|])界定。引用数组元素的语法,数组标识符的名字,加点,再加放在方括号([])中的元素索引;读元素值的语法也就是这个。设置元素值的语法,左箭头(<-),加上指定给这个元素的值。

下面的例子演示了读写数组。首先,定义一个数组rhymeArray,然后,读取数组中的所有成员,再向数组中插入新值,最后,输出所有值。

 

// define an array literal

let rhymeArray =

  [|"Went to market";

    "Stayedhome";

    "Hadroast beef";

    "Hadnone" |]

 

// unpack the array into identifiers

let firstPiggy = rhymeArray.[0]

let secondPiggy = rhymeArray.[1]

let thirdPiggy = rhymeArray.[2]

let fourthPiggy = rhymeArray.[3]

 

// update elements of the array

rhymeArray.[0] <- "Wee,"

rhymeArray.[1] <- "wee,"

rhymeArray.[2] <- "wee,"

rhymeArray.[3] <- "all the wayhome"

 

// give a short name to the new linecharacters

let nl = System.Environment.NewLine

// print out the identifiers & array

printfn "%s%s%s%s%s%s%s"

firstPiggy nl

secondPiggy nl

thirdPiggy nl

fourthPiggy

printfn "%A" rhymeArray

 

运行结果如下:

 

Went to market

Stayed home

Had roast beef

Had none

[|"Wee,"; "wee,";"wee,"; "all the way home"|]

 

像列表一样,数组也使用类型参数化(type parameterization),因此,数组内容的类型也就构成了数组的类型。写作内容类型,加上数组类型。因此, rhymeArray 的类型是字符串数组(string array),也可写成string[]。

F# 中的多维数组有两种不同的类型,不规则和矩形(jagged and rectangular)。不规则数组,正如它的名字所暗示的,它的第二维不是一个规则的形状。相反,它们是这样的数组,其内容也是其他数组,但内部数组的长度并不强求一致。在矩形数组中,所有内部数组长度相同。事实上,并没有真正内部数组的概念,因为整个数组就是同一个对象。在读写(getting and setting items)的方法上,两种数组也有一些不同。

对于不规则数组,用点,加上在方括号中的索引,但必须使用两次(每一维一次),因为第一次取到内部数组,第二次才取到其中的元素。

下面的例子演示用两种不同的方法,访问一个简单的不规则数组jagged 中的成员。第一个内部数组(索引为0)指定给标识符singleDim;然后,把它的第一个元素指定给itemOne。在第四行,只用一行代码,把第二个内部数组的第一个元素指定给itemTwo。

 

// define a jagged array literal

let jagged = [| [| "one" |] ; [|"two" ; "three" |] |]

// unpack elements from the arrays

let singleDim = jagged.[0]

let itemOne = singleDim.[0]

let itemTwo = jagged.[1].[0]

// print some of the unpacked elements

printfn "%s %s" itemOne itemTwo

 

运行结果如下:

 

one two

 

引用矩形数组中的元素,用点,加上所有的索引,放在方括号中,用逗号分隔。不像不规则数组,它虽是多维数组,但[||]的使用方法的语法却像定义一维数组一样;创建矩形数组,必须用Array2D、Array3D 模块中的create 函数,因为它分支支持二维、三维数组。但这产不表示矩形数组被限制为三维。因为使用System.Array 类,可以创建超过三维矩形数组;然而,创建这样的数组应该小心,因为,加上一维,会导致对象很快变得相当大。

下面的例子创建一个矩形数组square,然后,输出其元素:

 

// create a square array,

// initally populated with zeros

let square = Array2D.create 2 2 0

// populate the array

square.[0,0] <- 1

square.[0,1] <- 2

square.[1,0] <- 3

square.[1,1] <- 4

// print the array

printfn "%A" square

 

 现在,让我们看一下不规则数组与矩形数组的不同。首先,创建一个不规则数组,去表现Pascal的三角形数组;然后,创建一个矩形数组,包含不同数字的序列,隐藏在pascalsTriangle 中:

 

 

// define Pascal's Triangle as an

// array literal

let pascalsTriangle =

  [|

    [|1|];

    [|1; 1|];

    [|1; 2; 1|];

    [|1; 3; 3; 1|];

    [|1; 4; 6; 4; 1|];

    [|1; 5; 10; 10; 5; 1|];

    [|1; 6; 15; 20; 15; 6; 1|];

    [|1; 7; 21; 35; 35; 21; 7; 1|];

    [|1; 8; 28; 56; 70; 56; 28; 8; 1|]; |]

 

// collect elements from the jagged array

// assigning them to a square array

let numbers =

  letlength = (Array.length pascalsTriangle) in

  lettemp = Array2D.create 3 length 0 in

  forindex = 0 to length - 1 do

    letnaturelIndex = index - 1 in

    ifnaturelIndex >= 0 then

     temp.[0, index] <- pascalsTriangle.[index].[naturelIndex]

    lettriangularIndex = index - 2 in

    iftriangularIndex >= 0 then

     temp.[1, index] <- pascalsTriangle.[index].[triangularIndex]

    lettetrahedralIndex = index - 3 in

    if tetrahedralIndex >= 0 then

    temp.[2,index] <- pascalsTriangle.[index].[tetrahedralIndex]

  done

  temp

 

// print the array

printfn "%A" numbers

 

运行结果如下:

 

[|[|0; 1; 2; 3; 4; 5; 6; 7; 8|];[|0; 0; 1;3; 6; 10; 15; 21; 28|];

 [|0;0; 0; 1; 4; 10; 20; 35; 56|]|]

 

当使用编译器的-i 开关,可以显示如下的类型:

 

val pascals_triangle : int array array

val numbers : int [,]

 

正如你所期望的,不规则数组和矩形数组有不同的类型。不规则数组与一维数组相同,除了它的每一维是一个数组以外,因此,pascalsTriangle 的类型是int array array。而矩形数组使用的符号更像C#。首先,是数组元素类型的名字,然后是放在方括号中的维度,维度超过 1 的,每一维用逗号隔开。因此,例子中二维数组numbers 的类型是int[,]。

 

 

数组推导(array Comprehensions)

 

在第三章中我们讨论过列表、序列的推导语法,对应的语法也可以用来创建数组,它们之间的不同是界定数组的字符,这也是函数风格的语法所决定的,数组使用括号前加竖线括起来:

 

// an array of characters

let chars = [| '1' .. '9' |]

// an array of tuples of number, square

let squares =

  [|for x in 1 .. 9 -> x, x*x |]

// print out both arrays

printfn "%A" chars

printfn "%A" squares

 

运行结果如下:

 

[|'1'; '2'; '3'; '4'; '5'; '6'; '7'; '8';'9'|]

[|(1, 1); (2, 4); (3, 9); (4, 16); (5, 25);(6, 36); (7, 49); (8, 64); (9, 81)|]

0 0
原创粉丝点击