第三章 函数编程(一)

第三章函数编程

 

第一章,我们讲过,纯函数编程把一切都当成值对待,包括函数。虽然 F# 不是纯函数语言,但是,同样鼓励你以函数风格编程,即使用只有一个返回结果的表达式和计算,而不用有副作用的语句。在这一章,我们将介绍 F# 支持的、函数编程模式的主要语言结构,学习如何使函数风格编程更容易。

 

 

文字

 

文字(Literals),表示常量值,用于计算的构建块。F# 的文字很丰富,见如表3-1。

表3-1 F# 文字

示例

F# 类型

.NET 类型

描述

"Hello\t", "World\n”

string

System.String

字符串,其中的反斜杠(\) 是转义字符

@"c:\dir\fs", @""""

string

System.String

字符串,其中的反斜杠(\) 是正常字符

"bytesbytesbytes"B

byte array

System.Byte[]

字符串作为字节数组存储

'c'

char

System.Char

字符

true, false

bool

System.Boolean

布尔

0x22

int/int32

System.Int32

十六进制整数

0o42

int/int32

System.Int32

八进制整数

0b10010

int/int32

System.Int32

二进制整数

34y

sbyte

System.SByte

有符号字节

34uy

byte

System.Byte

无符号字节

34s

int16

System.Int16

16 位整数

34us

uint16

System.UInt16

无符号 16 位整数

34l

int/int32

System.Int32

32 位整数

34ul

uint32

System.UInt32

无符号 32 位整数

34n

nativeint

System.IntPtr

 本机大小(native-sized) 整数

34un

unativeint

System.UIntPtr

无符号本机大小整数

34L

int64

System.Int64

64 位整数

34UL

uint64

System.Int64

无符号 64 位整数

3.0F, 3.0f

float32

System.Single

32 位 IEEE 浮点数

3.0

float

System.Double

64 位 IEEE 浮点数

3474262622571I

bigint

Microsoft.FSharp.Math.BigInt

大整数

474262612536171N

bignum

Microsoft.FSharp.Math.BigNum

大数字

 

在F# 中,字符串可以包含换行符,正常的字符可以包含标准转义符,Verbatim字符串使用反斜杠(\)作为正常字符,两个双引号("")转义为一个双引号。用适当的前缀和后缀,整型可以定义为八进制、十六进制。下面示例展示了这些文字的部分应用,学习一下F# 的 printf 函数如何用%A 输出到控制台。Printf  函数解释 %A 格式模式,通过使用 F# 的反射(reflection,第七章中讨论)和 .NET的 ToString 方法进行组合,适用于所有类型,以可读的方式输出值。

 

// 字符串

let message = "Hello

World\r\n\t!"

let dir = @"c:\projects"

// 字节数组

let bytes = "bytesbytesbytes"B

// 数值类型

let xA = 0xFFy

let xB = 0o7777un

let xC = 0b10010UL

// 输出结果

let main() =

printfn “%A” message

printfn “%A” dir

printfn “%A” bytes

printfn “%A” xA

printfn “%A” xB

printfn “%A” xC

// 调用 main 函数

main()

 

示例编译并执行以后的结果如下:

 

"Hello

World

!"

"c:\projects"

[|98uy; 121uy; 116uy; 101uy; 115uy; 98uy;121uy; 116uy; 101uy; 115uy; 98uy;

121uy; 116uy; 101uy; 115uy|]

-1y

4095un

18UL

 

 

函数

 

在F# 中,字义函数使用 fun 关键字,函数参数之间用空格分隔,参数与函数体之间用(->)隔开。

下面是一个函数的示例,它取两个值,并把它们加起来。

 

fun x y -> x + y

 

注意,这个函数没有名字,这是一类函数文字。以这种方式定义的函数称为匿名函数(anonymous functions)、lambda 函数,或者干脆就叫lambdas。

函数不需要名字,这种想法看起来有点怪。然而,如果一个函数被当作参数传递给另一个函数,就可能不需要名字,特别是,它所承担的任务还可能相当简单。

如果需要给函数一个名字,你可以把它绑定给一个标识符,下一节再讨论。

 

 

标识符

 

F# 中,标识符是对值的命名,因此,在后面的程序中可以引用它们。标识符定义,用关键字 let,后面跟标识符名,等于号,表达式,它指定该标识符所引用的值。表达式可以任意的一段代码,表示将要返回值的计算。下列表达式显示一个值赋给标识符:

 

let x = 42

 

对于大多数有命令编程背景的人来说,这就像变量赋值。是有许多相似,但有一个关键的不同,

在纯函数编程中,一旦值赋给标识符,就不再改变。这也就是为什么我在全书中都称它们为标识符,而不是变量。

 

注意,在某些情况下,可以重新定义标识符,这看起来有点像是标识符的值改变了,但是,还是有些不同。另外,在 F# 中的命令编程,在某些情况下,标识符的值可以改变。在这一章,我们只关注函数编程,在这里,标识符的值不能改变。

 

一个标识符既可以指向值,也可以指向函数,因为F# 的函数本质上就是值,这毫不奇怪。这就是说,F# 没有真正的函数名或参数名的概念,它们统称为标识符。可以把一个匿名函数到标识符,用同样的方法,也可以把一个字符串或整数绑定到标识符:

 

let myAdd = fun x y -> x + y

 

然而,由于定义有名字的函数太常用了,因此,F# 为此提供了一个快捷语法。写函数定义的方法与值标识符相同,除非函数在 let 关键字和等号之间有两个或更多的标识符,就像下面的代码:

 

let raisePowerTwo x = x ** 2.0

 

 第一个标识符是函数名,raisePowerTwo,后面的标识符是函数的参数名,x。如果函数有名字,强烈建议你用这种快捷语法来定义。

在 F# 中,声明值和函数非常简单,因为函数就是值,F# 语法认为它们相同。例如,考虑下面的代码:

 

let n = 10

let add a b = a + b

let result = add n 4

printfn "result = %i" result

 

第一行,把10  指定给标识符n;

第二行,定义函数add,完成两个参数相加。注意语法多么相似,唯一不同的是,函数有参数,列在函数名的后面。由于在F# 中一切都是值,第一行中的文字10 是值,第二行中的表达式 a+b 的结果也是值,自动成为函数 add 的结果。

注意,与命令语言不同,函数不需要显式返回值。

这段代码编译并执行以后,结果如下:

 

result = 14

 

 

标识符的命名

 

标识符的命名要符合一定的规则。标识符必须以下划线(_)或字母开头,可以包含字母、数字、下划线,或单引号(’);关键字不能作标识符。由于 F# 可以把单引号作为标识符名字的一部分,这样,能够用它来表示“重要的部分”,使创建的标识符名字相似而不同,比如:

 

let x = 42

let x' = 43

 

F# 支持 Unicode,因此,可以用非拉丁字母文字作为标识符的名字:

 

let 标识符 = 42

 

如果你觉得这些规则过于严苛,可以使用双反撇号(``,[即键盘左上角 1 旁边的那个字符])把标识符名字括起来,这样,就可以使用任意字符序列作标识符的名字,只要不包括tabs、回车,或双反撇号。就是说,标识符的名字能够以问号结尾,例如(有些程序员认为,表示布尔值的名字能以问号结尾会更有意义):

 

let ``more? `` = true

 

这样,还可把关键字作为标识符或类型的名字:

 

let ``class`` = "style"

 

例如,你可能需要使用库函数中的成员,而这个库函数[由于中文的原因,library 有时翻译成库,有时翻译成库函数,并无区别。]不是用 F# 写的,并且正好使用了 F# 中的关键字作名字(我们将在第四章介绍非 F# 库函数)。不过,通常情况下,最好还是要避免过度使用这个功能,因为这会导致从其他 .NET 语言中使用这些库函数的困难。

 

 

作用域(Scope)

 

标识符的作用域定义了程序中哪些地方可以使用标识符(或者类型,看本章后面的“定义类型”一节)。理解得好是相当重要的,因为如果使用的标识符不在作用域中,会产生编译错误。

所有的标识符,不管是与函数相关,还是与值相关,范围是从它的定义开始,直到在它出现的这一段的结束。因此,对于顶级的标识符(相对于其他的函数、值来说,这些标识符不是本地的),这个标识符的作用域,是从它定义开始,直到源文件结束。顶级标识符一旦指定给值(或函数),这个值就不能改变或重新定义。标识符只能在它定义完成后才可以使用,就是说,通常情况下,不能根据自身定义标识符。

你将会注意到,在 F# 中,从来不需要显式返回值,计算结果会自动绑定到它所指定的标识符。那么,如何在函数中计算中间值呢?F# 通过空格进行控制。一个标识就创建了一个新的作用域,这个作用的结束由这一段的缩进的结尾所决定。缩进表示这个let 绑定在这一个计算中是中间值,在这个作用域之外是不可用的。当作用域结束(即这一段缩进的结束),标识符就不再可用,就是说,已经超出作用域,或者不在作用域内。

为了说明作用域,看一下这个例子,这个函数计算两个整数的中间数,第三、四行显示的是如何计算中间值。

 

//计算中点的函数

let halfWay a b =

let dif = b - a

let mid = dif / 2

mid + a

// 计算函数并打印结果

printfn "(halfWay 5 11) = %i"(halfWay 5 11)

printfn "(halfWay 11 5) = %i"(halfWay 11 5)

 

首先,这两个被计算的数字是不一样的。标识符dif 是用关键字 let 指定的,它是函数的中间值,有四个空格的缩进。具体缩进几个空格,由程序员决定,但是,通常是四个;然后,计算中点,指定给标识符 mid,使用同样的缩进;最后,函数的期望结果是把这个中点再加上 a,代码直接写成 mid + a,它就成为函数的结果。

 

注意,不能使用 tabs 代替缩进,因为,它在不同的文本编辑器中看起来可能不同,当空白有意义时,它会引发问题。[因此,书中提到的空白,全部译成空格。]

 

程序的结果:

 

(halfWay 5 11) = 8

(halfWay 11 5) = 8

 

 

F# 的轻量语法

 

默认情况下,F# 中的空格是有意义的,通常缩进控制标识符的作用域。F# 是基于Objective Caml (OCaml)语言的,而它的空白是无意义的,在 OCaml 中,作用域是通过关键字in 控制的。例如,前面的 halfWay 函数就可能变成下面这样(在中间两加上关键字 in):

 

let halfWay a b =

let dif = b - a in

let mid = dif / 2 in

mid + a

 

F# 空格有意义的语法被称为轻量语法,因为有些关键字和符号可以省略了,比如:in、;、begin、end。就是说,前面的函数定义,即使有关键字 in,F# 的编译器也是可以接受的。如果打算强制使用这些关键字,需要在每个源文件的开始加上#light "off" 的声明。

我认为,空格有意义的编程方法更加直观,因为,它可以很容易帮助程序员决定如何布局代码,因此,本书只讨论 F# 的轻量语法。

 

函数中的标识符被限定在表达式的结束,通常,被限定为所在函数定义的结束。因此,定义在函数内的标识符,在函数外边不能使用。看一下这个例子:

 

let printMessage() =

let message = "Help me"

printfn “%s” message

printfn “%s” message

 

企图在函数 printMessage的外边使用的标识符 message,超出了作用域,编译这段代码,会出错:

 

Prog.fs(34,17): error: FS0039: The value orconstructor 'message' is not defined.

 

函数内部的标识符与顶级标识符有一些不同,它们可以用let关键字重新定义。就是说,不需要一直保留为了处理中间值而定义的标识符名字。下面的例子演示了用F# 函数实现的一个数学谜题。这里,需要计算大量的中间值,但我们并不特别关心这些值,如果为每一个中间值都命名,实在不必要,那样,对程序员实在是一个负担。

 

open System

let readInt() = int (Console.ReadLine())

let mathsPuzzle() =

printfn "Enter day of the month on which you were born: "

let input = readInt ()

let x = input * 4 // Multiply it by 4

  letx = x + 13 // Add 13

let x = x * 25 // Multiply the result by 25

let x = x - 200 // Subtract 200

printfn "Enter number of the month you were born: "

let input = readInt ()

let x = x + input

let x = x * 2 // Multiply by 2

let x = x - 40 // Subtract 40

let x = x * 50 // Multiply the result by 50

printfn "Enter last two digits of the year of your birth:"

let input = readInt ()

let x = x + input

let x = x - 10500 // Finally, subtract 10,500

printf "Date of birth (ddmmyy): %i" x

mathsPuzzle()

 

运行的结果如下:

 

Enter day of the month on which you wereborn: 23

Enter number of the month you were born: 5

Enter last two digits of the year of yourbirth: 78

Date of birth (ddmmyy): 230578

 

注意:这不同于改变标识符的值。因为重新定义标识符,可能会改变标识符的类型,但仍然是类型安全的,比如下面的示例。

 

注意

类型安全(Type safety),有时也叫强类型(strong typing),主要是说,F# 会阻止你对值进行不适当操作。例如,不能把浮点数当成整数。在本章后面的“类型和类型推断”一节会讨论类型和如何保证类型安全。

 

let changeType () =

let x = 1 // bind x to an integer

let x = "change me" // rebind x to a string

let x = x + 1 // attempt to rebind to itself plus an integer

printfn x

 

这个例子不能编译,因为第三行把值 x 从整数改成了字符串"change me",而第四行尝试对字符串和整型进行加法运算,在 F# 中是非法的,因此,编译器会出错:

 

prog.fs(55,13): error: FS0001: Thisexpression has type

int

but is here used with type

string

stopped due to error

 

如果重新定义标识符,它的原来值在这个标识符定义的过程中仍然是可用的,但定义完成之后,即这个表达式的结束,就不可用了,原来值就被隐藏了。如果这个标识符在一个新的作用域内重新定义,那么,当这个新作用域结束以后,这个标识符还是原来的值。

下一个例子定义了标识符 message ,并把它输出到控制台,然后,在内部函数 innerFun 中重新定义message,它也打印这个 message。接着,调用函数innerFun,最后,第三次打印message。

 

let printMessages() =

// define message and print it

let message = "Important"

printfn "%s" message;

// define an inner function that redefines value of message

let innerFun () =

let message = "Very Important"

printfn "%s" message

// call the inner function

innerFun ()

// finally print message again

printfn "%s" message

 

printMessages()

 

下面是执行的结果:

 

Important

Very Important

Important

 

命令编程的程序员可能认为最后输出的message应该是Very Important,而不是Important。因为标识符message 保存的值Important,在函数innerFun内部,是被重新绑定到值Very Important,而不是赋值,这个绑定只有在函数innerFun 的作用域内才有效,因此,函数一旦结束,标识符message 的值重新回到原始值。

 

注意

使用内部函数是常见而有效的办法,可以把一些大的功能拆分成易于管理小块,你会看到本书中大量使用。它们有时也被称为闭包(closures)或 lambdas,虽然这是两个术语有更特殊的意义。闭包意思是这个函数使用了一个在顶层没有定义的值;而lambdas 即匿名函数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值