首页 > 其他 > 详细

第三章函数编程(二)

时间:2014-03-16 20:41:32      阅读:467      评论:0      收藏:0      [点我收藏+]

第三章函数编程(二)


捕获标识符(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# 中,可以把运算符看作是更优美的函数调用方法。

F# 有两种不同类型的运算符:

前缀(prefix)运算符,它的运算对象(operand)在这个运算符的后面;

中缀(infix)运算符,这个运算符在第一和第二个运算对象之间。

F# 提供了丰富多样的运算符集合,可用于数字、布尔值、字符串和集合类型。在 F# 和它的库函数中定义的运算符数量甚众,限于篇幅,在此不再一一详解,本文将着重介绍如何在 F# 中使用和定义运算符。

就像在C# 一样,F# 的运算符也可以重载(overload),就是说,一个运算符可以用于多种的类型;然而,与C# 不同的是,各个运算对象必须有相同的类型,否则会产生编译错误。F# 也允许用户定义、重定义运算符。

在这一节的最后会有讨论。

F# 的运算符重载规则与 C# 类似,因此,任何 BCL 类、.NET 库函数,在 C# 中支持运算符重载的运算符,在 F# 中也一样支持。例如,可以用+ 运算符去连接字符串,同样,也能对日期时间类型(System.DataTime)和时间段(System.TimeSpan)进行加,因为这些类型都支持+ 运算符的重载。下面的例子就演示了这些重载:

 

let ryhm = "Jack " + "and" + "Jill"

 

open System

let oneYearLater =

  DateTime.Now+ new TimeSpan(365, 0, 0, 0, 0)

 

与函数不一样,运算符不是值,因此,它就不能当作参数传递给其他函数。然而,如果真的需要把一个运算符当成值来使用,只要把它用括号括起来就行了,现在,这个运算符的行为就与函数完成一样了。这样,会有两个推论:

1、运算符现在已经是函数了,其函数必须放在运算符的后面:

let result = (+) 1 1

2、由于运算符是值,因此,它可以作为函数的结果返回,传递给其他的函数,或者绑定到标识符。这样定义 add 函数就非常简洁了:

let add = (+)

 

我们将会这一章的后面看到,当处理[ 由于中文的原因,有时翻成处理,有时翻成使用。]列表时,把运算符当作值是非常有用的。

只要用户愿意,既可以定义运算符,也可重新定义已有的运算符(虽然,这样做并不总是可取的,因为,这样的运算符不再支持重载)。下面的例子故意重把+ 定义为减法。

 

let (+) a b = a - b

printfn "%i" (1 + 1)

 

用户定义(custom自定义)运算符必须是非字母、数字,可以是一个字符,也可以是一组字符。可以用下列字符自定义运算符:

!$%&*+-./<=>?@^|~

[

这里有个较大的变化,原来还有一个冒号,但是不能是字符组的首字符;现在,干脆取消了。

]

定义运算符的语法与用关键字let定义函数相同,除了用运算符替换函数名,并用括号括起来,让编译器知道括号中的符号是运算符的名字,而不是运算对象。下面的例子自定义了运算符+:*,完成运算对象先加再乘:

 

let ( +* ) a b = (a + b) * a * b

printfn "(1 +* 2) = %i" (1 +:* 2)

 

例子的运行结果如下:

 

(1 +* 2) = 6

 

一元运算符总是在运算对象的前面。自定义的二元运算符,如果以感叹号(!)、问号(?)、波浪号(~)开头,就是前缀运算符;其他的则是中缀运算符,放在运算对象的中间。

 

 

函数应用(Function application)

 

函数应用,有时称为函数组合(function composition),或者组合函数(composing functions),简单地说,就是调用带有参数的函数。下面的例子定义了函数 add,然后应用两个参数。注意,参数没有用括号或逗号分隔,只需要用空格分隔。

 

let add x y = x + y

let result = add 4 5

printfn "(add 4 5) = %i" result

 

函数的运行结果如下:

 

(add 4 5) = 9

 

F# 的函数,如果有固定数量的参数,会直接应用源文件中接下来的值,调用函数时不需要使用括号;有时,使用括号,是为了说明函数应用了哪些参数。看一下这个例子,用 add 函数实现四个数的加法,可以为每一个函数调用绑定一个标识符,但是,针对这样一个简单的计算,这样做有点太啰嗦了:

 

let add x y = x + y

let result1 = add 4 5

let result2 = add 6 7

let finalResult = add result1 result2

 

相反,更好的办法通常是把一个函数的结果直接传递给下一个函数。要这样做,就要用括号说明哪些参数与该函数相关:

 

let add x y = x + y

let result =

  add(add 4 5) (add 6 7)

 

这里,add 函数的第二、第三个位置分别用括号把4、5 和 6、7 分了组,第一个位置将根据其他两个函数的结果进行计算。

F# 还有一种组合函数的方法,使用 pipe-forward运算符(|>),它的定义是这样的:

 

let (|>) x f = f x

 

简单地说,它取一个参数 x,应用到给定的函数 f,这样,参数就可以放在函数的前面了。下面的例子使用|> 运算符,把参数 0.5 应用到函数 System.Math.Cos:

 

let result = 0.5 |> System.Math.Cos

 

在某些情况下这种反转可能是很有用的,特别是打算把许多函数链接到一起时。下面是用 |> 运算符重写的前面 add 函数示例:

 

let add x y = x + y

let result = add 6 7 |> add 4 |> add5

 

有些程序员认为这种风格更具可读性,因为,因为它比以从右到左的方式读代码更方便。现在,这段代码可以读作“6 加 7,然后,把结果转交给下一个函数,再加 4,然后,再把结果转交给函数,加 5”。更多有关这种函数应用风格的适用环境我们放到第四章讲解。

这个示例还用到了 F# 的散函数应用,下一节再讨论。

 

 

函数的散应用(Partial Application of Functions)

 

F# 支持函数的散应用(有时也称为散函数,或curried 函数)。即,不必要给函数一次传递所有的参数。注意,前一节最后的示例,只传递一个参数给 add 函数,而它有两个参数。这是与函数就是值的观点相关。

因为函数就是值,如果它没有一次接受所有的参数,那么,它返回的值就是一个新函数,等着接受其余的参数。这样,在这个例子中,给 add 函数只传递 4,结果是一个新函数,我们把它称为 addFour,因为它只取一个参数,并把它加 4 。乍看起来,这个思想是无趣、无益的,但是,它是函数编程中的强大部分,在全书中都有应用。

这种行为不可能总是适当的,例如,如果这个函数有两个浮点参数表示一个点,那么,可能不希望这些数值分别传递给函数,因为,只有它们在一起才能表示点。另外,也可以用括号把函数的参数括起来,用逗号分隔,把它们变成一个元组(tuple)。请看下面的代码:

 

let sub (a, b) = a - b

let subFour = sub 4

 

当编译这个例子,会出现下面的错误消息:

 

prog.fs(15,19): error: FS0001: Thisexpression has type

int

but is here used with type

‘a * ‘b

 

这个示例不能编译,因为 sub 函数要求一次给足两个参数。现在 sub只有一个参数,元组(a,b),而不是两个参数。然而,在第二行调用 sub 时只提供了一个参数,且不是元组。因此,程序不能通过类型检查,因为,代码试图把一个整数传递给需要元组的函数。元组会在本意后面定义类型一节有更详细的讨论。

通常,能够被散应用的函数,要好于使用元组的函数。这是因为能够被散应用的函数比元组更有灵活性,给用户使用函数时有更多的选择。当为其他程序员提供库函数时,尤其重要,你无法预料用户使用函数的所有可能,因此,最好的办法使函数能够散应用,以增加灵活性。

 

 

模式匹配(Pattern Matching)

 

模式匹配首先看一下标识符的值,然后,根据不同的值采取不同的计算。它有点像C++ 和 C# 中的switch 语句,但是更有效、更灵活。用函数风格写的程序更趋向于写应用于输入数据的转换的一系列。模式匹配能够分析输入数据,决定应用哪一个转换,因此,模式匹配非常适合函数编程风格。

F# 的模式匹配构造可以模式匹配多种类型和值,它有几种不同的形式,会出现在语言的几个地方,包括异常处理的语法,在本章的后面“异常和异常处理”一节会有讨论。

最简单的模式匹配形式是匹配值,在本章前面“递归”一节已经看到,实现生成斐波那契序列数的函数。为解释这个语法,下面的例子实现产生卢卡斯(Lucas)数的函数,其序列数是这样的:1, 3, 4, 7, 11, 18, 29,47, 76, … 卢卡斯序列的定义和斐波那契序列一样,只是起点不同。

 

let rec luc x =

  matchx with

  | xwhen x <= 0 -> failwith "value must be greater than 0"

  | 1-> 1

  | 2-> 3

  | x-> luc (x - 1) + luc (- -x - 2)

// call the function and print the results

printfn "(luc 2) = %i" (luc 2)

printfn "(luc 6) = %i" (luc 6)

printfn "(luc 11) = %i" (luc 11)

printfn "(luc 12) = %i" (luc 12)

 

程序的运行结果如下:

 

(luc 2) = 3

(luc 6) = 18

(luc 11) = 199

(luc 12) = 322

 

模式匹配的语法使用关键字match,后面是被匹配的标识符,再后面是关键字with,然后,就是所有可能的匹配规则,用竖线(|)隔开。最简单的情况,规则由常数或标识符组成,后面跟箭头(->),然后是当值匹配时使用的表达式。在函数luc 的定义中,第二、第三种情况是两个文字,值1、2,分别用值1、3 替代。第四种情况,将匹配任意大于2 的值,会进一步两次调用lun 函数。

规则的匹配是按定义的顺序进行的,如果模式匹配不完整,编译器会报错,即,有些可能的输入没有匹配到任何规则。比如在luc 函数中省略了最后的规则,那么,任意大于2 的值x 就匹配不到任何规则;如果有些规则从未被匹配,编译器会报一个警告,典型的情况是在这个规则的前面已经有了一个更一般的规则。比如,把luc 函数中的第四个规则移到第一个规则的前面,那么,其他规则不会被匹配,因为第一个规则可以匹配任意的x 值。

可以添加when 子句(就像这个例子中第一个规则),去精确控制如何触发一个规则。when 子句的组成:关键字when,后面跟逻辑表达式。一旦规则匹配,when 子句被计算,如果表达式的结果为true,那么就触发规则;如果表达式的结果为false,就去匹配余下的规则。第一个规则是函数的错误控制。这个规则的第一部分是标识符,能够匹配任意整数,但是,有了when 子句,表示规则将只匹配小于等于0 的整数。

如果你愿意,可以省略第一个竖线。可用于模式匹配很小,想把它们写成一行时。下面的示例不仅除了使用这一项以外,演示了使用下划线(_)作为通配符:

 

let booleanToString x =

 match x with false -> "False" | _ -> "True"

 

 

_ 将匹配任意值,它告诉编译器你对这个值的使用不感兴趣。例如,在这个函数 booleanToString中,第二个规则中不需要使用常数true,因为,如果第一个规则[ 不 ]匹配,x的值将是true,而且,不需要通过x 得到字符串“True”,因此,可以忽略这个值,就用 _ 作为能配符。

模式匹配的另一个有用功能是用竖线把两个模式组合成一个规则。下面的例子stringToBoolean,就演示这个。

 

// function for converting a boolean to astring

let booleanToString x =

 match x with false -> "False" | _ -> "True"

 

// function for converting a string to aboolean

let stringToBoolean x =

  matchx with

  |"True" | "true" -> false

  |"False" | "false" -> true

  | _-> failwith "unexpected input"

// call the functions and print the results

printfn "(booleanToString true) =%s" (booleanToString true)

printfn "(booleanToString false) =%s" (booleanToString false)

printfn "(stringToBoolean\"True\") = %b" (stringToBoolean "True")

printfn "(stringToBoolean\"false\") = %b" (stringToBoolean "false")

printfn "(stringToBoolean\"Hello\") = %b" (stringToBoolean "Hello")

 

前面两个规则,是两个字符串应该得到相同的值,因此,不必要用两个单独的规则,只要在两个模式之间加上竖线。例子运行的结果如下:

 

(booleanToString true) = True

(booleanToString false) = False

(stringToBoolean "True") = true

(stringToBoolean "false") = false

Microsoft.FSharp.Core.FailureException:unexpected input

at FSI_0005.stringToBoolean(String x)

at<StartupCode$FSI_0005>.$FSI_0005.main@()

 

模式匹配可用于大多数F# 定义的类型。下面两个例子演示了关于元组的模式匹配,用两个函数通过模式匹配实现逻辑“与”和“或”,两者在实现上略有不同。

 

let myOr b1 b2 =

  match b1, b2 with

  | true, _ -> true

  | _, true -> true

  | _ -> false

let myAnd p =

  match p with

  | true, true -> true

  | _ -> false

printfn "(myOr true false) = %b"(myOr true false)

printfn "(myOr false false) = %b"(myOr false false)

printfn "(myAnd (true, false)) =%b" (myAnd (true, false))

printfn "(myAnd (true, true)) =%b" (myAnd (true, true))

 

程序运行结果如下:

 

(myOr true false) = true

(myOr false false) = false

(myAnd (true, false)) = false

(myAnd (true, true)) = true

 

 

myOr 函数有两个 Boolean 参数,放在关键字 match和 with 中间,用逗号隔开,形成元组;而myAnd 函数只有一个参数,本身就是元组。每一种方法,对创建元组模式匹配的语法是相同的,与创建元组的语法相似。

如果需要匹配元组中的值,常数或标识符要用逗号隔开,常数或标识符的位置定义了它要匹配元组中哪一项。如myOr 函数的第一、二个规则和myAnd 函数的第一规则,这些规则用常数匹配元组的一部分,如果想在规则中分别处理元组中各部分,可以使用标识符。仅仅是因为需要处理元组,但并不表示总是需要看到组成元组的各个部分。

myOr 的第三规则和 myAnd 的第二规则用通配符 _ 匹配整个元组,如果想在规则的后面用到元组中的值,也可用标识符替代。

由于模式匹配在F# 中是很常用的任务,因此,语言提供了快捷语法。如果函数的唯一目的就是针对某一件事的模式匹配,那么,使用这种语法是值得的。这个版本的模式匹配语法,用关键字function,把模式放到通常放函数参数的位置,然后,把所有可选的规则用竖线分开。下面的例子演示了这个语法,用一个简单的函数递归处理一个字符串列表,并把它连接成一个字符串。

 

let recconactStringList  =

  function  head :: tail -> head +conactStringList tail

            | [] -> ""

//test data

let jabber = ["‘Twas ";"brillig,";"and "; "the "; "slithy "; "toves "; "..."]

//call funcation

let completJabber =conactStringList jabber

//print the result

printfn"%s" completJabber

 

运行结果如下:

 

‘Twas brillig, and the slithy toves ...

 

模式匹配是 F# 的基础部分,在本章中还会有几个地方讲到。我们会看到针对列表、记录类型、联合类型、异常处理的模式匹配,模式匹配的最高级用法在本章最后的“活动模式”一节讨论,其他匹配的基础。在第四章要讨论针对非 F# 库函数中类型的模式匹配。

 

第三章函数编程(二),布布扣,bubuko.com

第三章函数编程(二)

原文:http://blog.csdn.net/hadstj/article/details/21333957

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!