第三章函数编程(二)
捕获标识符(Capturing Identifiers)
前面已经说过,F# 可以在函数内部再定义函数,这些函数可以使用作用域内的任何标识符,也包括本函数定义的本地标识符[由于汉语的原因,在不同语境下,本地与局部并不区分。]因为这些内部就是值,它们也可以成为这个函数的结果被返回,或者作为参数传递给其他函数。这就是说,虽然一个标识符定义在函数的内部,对其他的函数来说是不可见的,但是,它的生命期有可能会长于定义它的函数。我们用一个例子来说明,下面的定义了一个函数calculatePrefixFunction:
// function that returns a function to
let calculatePrefixFunction prefix =
// calculate prefix
let prefix' = Printf.sprintf "[%s]: " prefix
// define function to perform prefixing
let prefixFunction appendee =
Printf.sprintf "%s%s" prefix' appendee
// return function
prefixFunction
// create the prefix function
let prefixer = calculatePrefixFunction"DEBUG"
// use the prefix function
printfn "%s" (prefixer "Mymessage")
这个函数返回它定义的内部函数prefixFunction,标识符prefix' 对函数calculatePrefixFunction 的作用域来说,是本地的,在 calculatePrefixFunction之外的其他函数是看不到它的。而内部函数prefixFunction 还用到了 prefix',因此,当返回prefixFunction 时,值prefix' 必须仍然可用。用calculatePrefixFunction 创建了函数 prefixer,当调用 prefixer时,你会看到,它的结果使用了和prefix' 相关联的计算值:
[DEBUG]: My message
虽然你应该对这个过程有一个了解,但是,大多数情部下,你根本不需要为此而费心,因为它不需要程序员的任何额外的努力,编译器会自动生成一个闭包,扩展本地值的生命期,至其所定义的函数之外。因此,理解在闭包中捕获标识符的过程更为重要,当以命令风格编程时,其标识符可以表示随时间而改变的值;而以函数风格编程时,标识符总是表示值是常量,找出在闭包中到底捕获到了什么,可以更容易理解。
use 绑定
use 绑定可以用于标识符超出作用域之外执行一些动作。比如,在完成文件读写后关闭文件句柄,只要表示这个文件的标识符一超出作用域,就就把它关闭。更一般地,任何一个操作系统资源都是很宝贵的,可能是创建的代价大,比如网络套接字,也可能有数量上的限制,比如数据库连接,因此,应该尽可能快地关闭或释放。
在 .NET 中,属于这种类别的对象都应该实现IDisposable 接口(有关对象和接口的详细内容,参看第五章),这个接口包含一个方法Dispose,它负责清除资源。比如,如果是文件,它会关闭文件句柄。因此,在许多情况下,当标识符超出作用域时,应该调用这个方法。F# 中的 use 绑定就是做这个的。
use 绑定的行为与 let 绑定基本相同,除了当变量超出作用域时,编译器会自动生成代码,保证在作用结束后调用Dispose 方法,即使发生异常(有关异常的更多内容,参看本意后面异常和异常处理一节),编译器生成的代码会被调用。下面的例子演示了 use 绑定:
open System.IO
// function to read first line from a file
let readFirstLine filename =
// openfile using a "use" binding
use file =File.OpenText filename
file.ReadLine()
// call function and print the result
printfn "First line was: %s"(readFirstLine "mytext.txt")
这里,函数 readFirstLine 用 .NET 框架中的方法 File.OpenText 打开文本文件,访问其内容,标识符 file 使用 use 绑定了 OpenText 返回的 StreamReader,然后,从文件中读取第一行,作为结果返回。至此,标识符 file 已经超出作用域,因此,它的 Dispose 方法会被调用,后面文件句柄。
注意,使用 use 绑定有两个重要的限制:
1、只能对实现了 IDisposable 接口的对象使用 use 绑定;
2、不能在顶层使用 use 绑定,只能用在函数中,因为顶层的标识符永远不会超出作用域。
递归(Recursion)
递归,意思是函数根据它自己来定义,换名话说,函数在它的定义中调用了自己。递归通常用在函数编程中,而在命令编程中通常使用循环。许多人认为,用递归表达比循环的算法更容易理解。
在F# 中使用递归,在关键字 let 后再加上关键字 rec,使标识符在函数定义可用。下面是使用一个递归的例子,注意只用五行语句,在函数定义中两次调用它自己。
let rec fib x =
matchx with
| 1-> 1
| 2-> 1
| x-> fib (x - 1) + fib (x - 2)
// call the function and print the results
printfn "(fib 2) = %i" (fib 2)
printfn "(fib 6) = %i" (fib 6)
printfn "(fib 11) = %i" (fib 11)
结果如下:
(fib 2) = 1
(fib 6) = 8
(fib 11) = 89
这个函数是计算斐波那契(Fibonacci)数列第n 项的值。斐波那契数列的每一项由它前面的两个数相加而得,它的过程像这样:1, 1, 2, 3, 5, 8, 13, ... 递归最适合计算斐波那契数列,因为数列中的任一数,除了最初两个数以个,都可以通过它前面的两个数计算而得,因此,斐波那契数列是根据它自己定义的。
虽然递归是一个很强大的工具,但使用还是要小心。因为一不留神很容易就会写出一个永不终止的递归函数。虽然,刻意写一个永不终止的递归函数有时也是有用的,并不常见,只在试算时会用到。要保证递归函数终止,应该确定基本项和递归项。
递归项,即定义值的函数项中包含它自己。例如,函数 fib,是除第1、2 以外的任意值;
基本项,非递归项,即必须有某个值,其定义函数不含它自己。在函数fib 中,1、2 项就是基本项。
光有基本项,还不能根本保留递归能终止,递归项还必须有向基本项的趋势。在fib 例子中,如果x 大于等于3,递归项将趋向基本项,因为x 总是变得更小,最终到达2;然而,如果x 小于1,那么,x 就会变成负数,绝对值越来越大,函数会一直计算下去,直到机器资源耗尽,堆栈溢出(System.StackOverflowException)。
前面的代码还用到了F# 的模式匹配,将在这一章后面的“模式匹配”一节中讨论。
运算符(Operators)
在F# 中,可以把运算符看作