代码改变世界

F#探险之旅(三):命令式编程(上)

2008-09-25 01:56 Anders Cui 阅读(...) 评论(...) 编辑 收藏

F#系列随笔索引页面

在本系列的第二部分(函数式编程)中,我们了解了如何使用F#进行纯粹的函数式编程。但是在一些情况下,比如I/O,几乎不能避免改变状态,也就是说会带来side effect。F#并不强求你以无状态的方式编写程序,它提供了可修改(mutable)的标识符来解决这类问题,同时它还提供了其它的程序结构以支持命令式编程。现在就来对这些特性探个究竟。

首先是unit类型,这种类型表示“没有值”。然后是F#如何处理可修改的值。最后来看看如何在F#中使用.NET类库,包括如何调用静态方法、创建对象并使用其成员、使用类的索引器和事件以及F#中的|>操作符。

unit类型

没有参数和返回值的函数的类型为unit,它类似于C#中的void,或者说CLR中的System.Void。对于使用函数式编程语言的开发人员来说,不接受参数、无返回值的函数没多大意义。而在命令式编程中,由于side effect的存在,这样的函数仍有意义。unit类型表示为一对括号“()”。

F# Code
let main() =
()


在这个例子中,main函数的类型为unit -> unit,也就是说它既不接受参数,也无返回值。第一对括号使得main成为一个函数而不是一个简单的值。第二对括号告诉编译器,main函数什么值也不返回。

注意:仅仅将函数命名为main不表示它就是程序的入口,它不会自动执行(像C#的Main方法那样),要执行它,需要在源文件的结尾处调用:main(),在后面的文章中将介绍如何指定F#程序的入口。

再来看看如何调用unit类型的函数,有两种方式:

F# Code
let () = main()
// -- or --
main()


我们还可以在函数内部连续多次调用unit类型的函数——只要保证它们的缩进是一样的即可:

F# Code
let poem() =
print_endline
"I sent thee late a rosy wreath"
print_endline
"Not so much honouring thee"
print_endline
"As giving it a hope that there"
print_endline
"It could not withered be"

poem()


这是《Song to Celia》中的四句诗词,print_endline函数的类型为string -> unit,我们知道函数的返回值为其函数体内最后一步运算的值,所以poem的类型为unit -> unit。

并不是说只有unit类型的函数可以这么用,但是调用非unit类型的函数时编译器会报告一个警告,因为它可能会有副作用(side effect),比如:warning FS0020: This expression should have type 'unit', but has type 'string',我们看到警告总会感觉不舒服。F#提供了一些机制可将这些警告消除,即将函数转换为unit类型的函数。事实上,这种需求在使用F#库时不多,在使用由其它语言编写的.NET类库时会更多些。看下面的例子:

F# Code
#light
let getString() = "foo bar"

let _ = getString()
// -- or --
ignore(getString())
// -- or --
getString() |> ignore


首先是函数getString的定义,下面的三行则是将其转换为unit类型函数的三种方式。第一种是使用下划线“_”,它在前面已经出现过几次,它一般表示我们对某些值不感兴趣;既然不感兴趣,那就可以忽略(ignore)了,这就是第二种方式:ignore函数;第三种方式使用了“|>”操作符,它本质上同第二种一样。“|>”操作符会在稍后介绍。

mutable关键字

在探险之旅(二)中我们知道可以使用let关键字将值绑定至标识符,在某些情况下,我们还可以重定义(redefine)标识符或者绑定(rebind)新的值,但是不能直接修改它的值。显然,对于我们这些用惯了命令式编程语言的人来说,这实在有些不爽,因为这些语言以修改变量的值作为最基本的运算方式。既然F#也支持命令式编程范式,它当然也能让你修改标识符的值。这就是mutable关键字和“<-”操作符,“<-”的类型为unit(操作符也是函数),下面的例子对此作了演示:

F# Code
let mutable phrase = "Good good study, "
print_endline phrase
phrase
<- "day day up."
print_endline phrase


运行结果为:

Output
Good good study,
day day up.


这看起来像是重定义标识符,其实不然。修改标识符时,只能修改它的值而不能修改类型;重定义时可同时改变类型和值(这本质上是定义了一个新的标识符)。事实上,如果你尝试修改标识符的类型,编译器会给你一个错误。另外,它们还有一个重要的差别,即它们的可见性(或者说修改行为的作用域)。在重定义标识符的时候,修改仅仅在新标识符的作用域内有效,一旦出了这个作用域,它就会回复到旧有的值;对于可修改的标识符来说,任何修改都是永久性,与作用域无关。

F# Code
let redefineX() =
let x = "One"
printfn
"Redefining: \r\nx = %s" x
if true then
let x = "Two"
printfn
"x = %s" x
else ()
printfn
"x = %s" x

let mutableX() =
let mutable x = "One"
printfn
"Mutating: \r\nx = %s" x
if true then
x
<- "Two"
printfn
"x = %s" x
else ()
printfn
"x = %s" x

redefineX()
mutableX()


运行结果为:

Output
Redefining:
X = One
X = Two
X = One
Mutating:
X = One
X = Two
X = Two


可修改的标识符也有其局限性,在子函数内不能修改它的值。而这也是ref类型的来由,稍后你会看到。

定义可修改的记录(Record)类型

默认情况下,记录类型是不可变的。不过F#提供了一种特殊的语法,使得我们可以修改记录类型的字段值,即在字段前使用mutable关键字。需要注意的是,这种操作改变的是记录字段的内容而不是记录本身

F# Code
type Couple =
{her :
string; mutable him : string}
let couple = {her = "Elizabeth Taylor"; him = "Nicky Hilton"}

let print o = printf "%A \r\n" o

let changeCouple() =
print couple;
couple.him
<- "Michael Wilding";
print couple;
couple.him
<- "Michael Todd";
print couple;

changeCouple()


通过Couple类型的定义可知,him字段是可修改的,就像changeCouple中的代码,但如果尝试修改her的值就会遭遇编译错误。

ref类型

ref类型是一种状态进行修改的简单方式。ref类型其实是包含一个可修改字段的record类型,它定义在F#库中,伴随它的还有两个操作符,它们使得操作ref类型更为方便:

F# Code
let ref x = { contents = x }
let (:=) x y = x.contents <- y
let (!) x = x.contents


ref“函数”将输入的值“装入”一个记录类型,同时用“:=”操作符来进行赋值,“!”来取值。进一步分析,ref“函数”的类型为a’ -> Ref<a’>,可以了解到“装入”的记录类型为Ref<a’>,由此可知使用了类型参数化(type parameterization),这个概念前面部分已经介绍过了。这意味着ref可以接受任意类型的值,但是一经赋值,其类型也就固定了。Ref<a’>类型暴露了Value属性,我们也可以通过它来获取或设置ref类型的值。

F# Code
let phrase = ref "Inconsistency"


考虑一个简单的问题,求一个整型数组所有元素的和。先看C#怎么做:

C# Code
static int TotalArray(int[] array)
{
int total = 0;
foreach (int element in array)
{
total
+= element;
}

return total;
}


再看看F#的版本:

F# Code
let totalArray (intArray : int array) =
let x = ref 0
for n in intArray do
x := !x + n
!x


可以看到F#的命令式编程范式下与C#何其相似!

数组(Array)

数组算得上是我们最熟悉的数据结构了。F#中的数组基于BCL中的System.Array类型,是一种可修改的集合类型。数组与列表相对,数组中的值是可修改的,而列表中的值则不能;列表的容量(长度)可以动态增大,数组则不能。一维数组又时被称为向量(Vector),多维数组有时被称为矩阵(Matrix)。定义数组时,将各项置于“[|”和“|]”中,各项间用“;”隔开。

下面的例子演示了如何对数组进行读取和写入操作。

F# Code
// 定义
let rhymeArray = [| "Hello"; "F#" |]

// 读取
let firstPiggy = rhymeArray.[0]
let secondPiggy = rhymeArray.[1]

// 写入
rhymeArray.[0] <- "Byebye"
rhymeArray.[
1] <- "my friend"

// 输出
print_endline firstPiggy
print_endline secondPiggy
print_any rhymeArray


数组跟列表一样,也采用了类型参数化,数组的类型为其元素的类型,因此rhymeArray的类型为string array,也可写作string[]。

F#中的多维数组可分为两类:交错数组(jagged array)和规则数组。交错数组,表示数组的数组,就是说最外部数组的元素也是数组(称为内部数组),内部数组的长度不必相同。而规则数组,事实上整个数组作为一个对象,其内部数组的长度是相同的。

先来看看交错数组的用法:

F# Code
let jaggedArray = [| [| "one" |]; [| "two"; "three" |] |]
let singleDimension = jaggedArray.[0]
let itemOne = singleDimension.[0]
let itemTwo = jaggedArray.[1].[0]

printfn
"%s %s" itemOne itemTwo // one two


jaggedArray的类型为string array array,这也是为什么说它是数组的数组,操作规则数组的语法有所不同:

F# Code
let square = Array2.create 2 2 0
square.[
0,0] <- 1
square.[
0,1] <- 2
square.[
1,0] <- 3
square.[
1,1] <- 4
printf
"%A \r\n" square // [| [|1; 2|]; [|3; 4|] |]


square的类型为int[,]。

注意:要编写.NET 1.1 和.NET 2.0兼容的代码,需要使用Microsoft.FSharp.Compatibility命名空间的CompatArray和CompatMatrix类。

数组推导(Array Comprehension)

前面介绍过了关于列表和序列的推导语法。我们也可以使用类似的语法进行数组推导。

F# Code
let chars = [|'1' .. '9'|]

let squares =
[
| for x in 1 .. 9
-> x, x * x |]
printfn
"%A" chars
printfn
"%A" squares


注意:本文中的代码均在F# 1.9.4.17版本下编写,在F# CTP 1.9.6.0版本下可能不能通过编译。

F#系列随笔索引页面

参考:
《Foundations of F#》 by Robert Pickering
《Expert F#》 by Don Syme , Adam Granicz , Antonio Cisternino
F# Specs