第四章 命令编程(一)
正如在第三章中所见,我们可以用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)|]
原文:http://blog.csdn.net/hadstj/article/details/21953833