代码改变世界

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

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

控制流程(Control Flow)

函数式编程(中)一文中,我们初步了解了F#在函数式编程范式下的控制流程,即if, elif, then, else等组成的结构。在命令式编程范式下,F#提供了更多的控制流程支持,包括if,while和for。

在命令式编程范式下的if结构与函数式编程下对应结构的主要差别在于,对于if分支,调用的函数为unit类型(即无返回值),而且并不要求必须使用else分支:

F# Code
if System.DateTime.Now.DayOfWeek = System.DayOfWeek.Thursday then
print_endline
"Thursday play list: lazy afternoon"


这里print_endline函数的类型为string -> int。尽管else分支不是必须的,但如果需要,你也可以加上,不过else分支也必须为unit类型。

F# Code
if System.DateTime.Now.DayOfWeek = System.DayOfWeek.Thursday then
print_endline
"Thursday play list: lazy afternoon"
else
print_endline
"Alt play list: pop music"


至此,不管if结构的分支是否返回值,我们都有办法表示,这样就跟C#的if结构一致了。

在C#中,如果一个分支的语句多于一条,需要使用花括号,而在F#中,分支所包含的语句要通过缩进来表示。

for循环是命令式编程中一种常见的结构。如果你有过C#或VB.NET的经验,那么很容易理解:

F# Code
let sentence = [| "To "; "live "; "is "; "to "; "function." |]

for index = 0 to Array.length sentence - 1 do
System.Console.Write sentence.[index]


在C#中,for循环是否执行需要看中间的bool表达式的结果,而这里则是看局部值index是否在指定的范围内,而且初始值要小于终止值。F#还提供了另一种for循环结构:

F# Code
let whitePollution = [| "This term refers to "; "pollution caused by ";
"litter of used plastic bags, "; "polystyrene cups, ";
"food containers and paper." |]

for index = Array.length whitePollution - 1 downto 0 do
System.Console.Write whitePollution.[index]


此时index的值将以递减顺序变化。

while循环也较为简单,与C#很相似,直接看个例子吧:

F# Code
// 压洲
let pressureContinent = ref [ "This phrase's pronunciation is ";
"similar to \"Asia\" in Chinese, "; "but it means ";
"a continent of pressure." ]

while (List.nonempty !pressureContinent) do
System.Console.Write(List.hd !pressureContinent);
pressureContinent := List.tl !pressureContinent


循环推导(Loops over Comprehensions)

可以使用for循环来枚举一个集合,这种方式与C#中的foreach结构类似。下面的例子对一个字符串数组进行枚举。

F# Code
let words = [| "Red"; "Lorry"; "Yellow"; "Lorry" |]

for word in words do
print_endline word


调用.NET类库中的静态方法和属性

F#中的命令式编程有一个极为有用的特性,它能够调用由任意.NET语言编写的类库,这包括BCL本身。不过在调用由F#编写的类库和由其它语言编写的类库时有所不同,因为F#类库拥有额外的元数据,比如一个方法是否接受一个元组或者其参数是否可被柯里化,这些元数据专用于F#。Microsoft.FSharp.Reflection API的产生很大程度上是由于这些元数据,这些API用于在F#和.NET的元数据间进行交互。

调用类的静态或实例方法和属性的基本语法是相同的,而在调用由非F#类库中的方法时必须使用括号(在F#中通常可用空格)。非F#类库中的方法不能被柯里化,方法本身也不是值,因此不能作为参数传递。遵循了这些规则,调用非F#类库的方法就变得直白、简单了。先来看看如何使用静态属性和方法:

F# Code
#light
open System.IO

if File.Exists("test.txt") then
print_endline
"test.txt is present"
else
print_endline
"test.txt does not exist"


Exists方法是File类的静态方法,这里的用法跟C#或VB.NET中很像。但这里的代码风格不太像函数式编程的风格,我们可以将.NET类库的方法做个简单的包装

F# Code
let exists filePath = File.Exists(filePath)
let files = ["test1.txt"; "test2.txt"; "test3.txt"]

let results = List.map exists files
print_any results


上面的代码功能是对列表中的文件逐一进行检测,看它是否存在。exists函数对Exist方法做了包装,这样就可以以函数式编程的风格调用它了。

如果调用的.NET方法有很多参数,可能会忘掉某个参数的用途,在VS中可以查看参数信息(快捷键Ctrl+K, P)。而在F#中我们还可以使用具名参数(named argumengs)

F# Code
open System.IO

let file = File.Open(path = "test.txt",
mode = FileMode.Append,
access = FileAccess.Write,
share = FileShare.None)

print_any file.Length
file.Close()


使用.NET类库中的对象和实例成员

除了类的静态成员,我们也可以创建类的实例并使用它的成员(字段、属性、事件、方法):

F# Code
#light
open System.IO

let file = new FileInfo("notExisting.txt")
if not file.Exists then
using(file.CreateText()) (
fun stream ->
stream.WriteLine(
"hello, f#"))
file.Attributes
<- FileAttributes.ReadOnly

print_endline file.FullName


这段代码引用命名空间,创建FileInfo类的实例,检查文件是否存在,(如果不存在的话)创建文件,写入文本,设置属性值,熟悉C#或VB.NET的你是否感觉很眼熟?这里创建的实例很像记录类型,它引用的对象本身(file)是不能修改的,但它包含的内容则是可以修改的(Attributes属性)。设置属性值时要用“<-”操作符。using其实是一个操作符,用于清理资源(对比下C#中的using语句)。

再考虑一下上面的例子,中间的两步是创建实例,设置属性,这是我们经常做的事情:

C# Code
Person person = new Person(1);
person.Name
= "Steve";
person.BirthOn
= DateTime.Now;


F#中还可以把上述过程简化:

F# Code
open System.IO
let fileName = "test.txt"
let file =
if File.Exists(fileName) then
Some(
new FileInfo(fileName, Attributes = FileAttributes.ReadOnly))
else
None


使用.NET类库中的索引器(Indexer)

索引器是.NET中的一个重要的概念,它使得一个集合类看起来像是一个数组。它本质上是名为Item的特殊属性。基于上述两点,F#提供了两种方式来访问索引器。

F# Code
#light
open System.Collections.Generic

let stringList =
let temp = new ResizeArray<string>() in
temp.AddRange([
|"one"; "two"; "three"|]);
temp

let itemOne = stringList.Item(0)
let itemTwo = stringList.[1]

printfn
"%s %s" itemOne itemTwo


第一种以属性的方式访问,第二种以数组的方式访问。

注意:上面例子中的代码很简单,却展示了F#中的一种常用模式。在创建标识符stringList时,首先将其实例化,然后调用它的实例成员(AddRange)设置状态,最后返回。

使用.NET类库中的事件(Event)

对于Windows Forms和Web Forms的开发人员,恐怕没有不知道事件的含义吧?我们可以将函数附加到事件上,比如Button的Click事件,这些附加的函数有时称为事件处理器(Event Handler)。

向事件添加一个处理器函数也很简单。每个事件都暴露了Add方法,由于事件在非F#类库中定义,因此Add方法需要带有括号。在F#中,通常可使用匿名函数作为处理器函数

下面的例子使用了Timer类及其Elapsed事件:

F# Code-Timer的Elapsed事件


能附加事件处理器,当然也能移除事件处理器,这需要使用RemoveHandler方法。RemoveHandler方法接受一个委托值,这个委托值封装了.NET中的方法,使得它可在方法间像值一样传递,不过在用RemoveHandler前要用AddHandler添加事件处理器,而不是Add方法

对.NET类型应用模式匹配

模式匹配使得我们可以针对不同的值进行不同的运算。此外,F#还允许对.NET类型进行匹配,这要用到:?操作符。

F# Code
#light
let simpleList = [box 1; box 2.0; box "three"]

let recognizeType (item : obj) =
match item with
| :? System.Int32 -> print_endline "An integer"
| :? System.Double -> print_endline "A double"
| :? System.String -> print_endline "A string"
| _ -> print_endline "Unkown type"


我们不可能罗列所有的.NET类型,最后一行的作用在于匹配所有的其它类型。很自然的,在对类型进行匹配时,我们不仅想知道类型,还想了解当前的值,可以这么做:

F# Code
#light
let simpleList = [box 1; box 2.0; box "three"]

let recognizeType (item : obj) =
match item with
| :? System.Int32 as x -> printfn "An integer: %i" x
| :? System.Double as x -> printfn "A double: %f" x
| :? System.String as x -> printfn "A string: %s" x
| x -> printfn "An object: %A" x


前面的文章中我们了解了异常处理的基本用法,这里的技术也可用在异常处理中,因为我们往往会根据类型捕获异常。

F# Code
let now = System.DateTime.Now
System.Console.WriteLine(now)

try
if now.Second % 3 = 0 then
raise (
new System.Exception())
else
raise (
new System.ApplicationException())
with
| :? System.ApplicationException ->
print_endline
"A second that was not a multiple of 3"
| _ ->
print_endline
"A second that was a multiple 3"


|> 操作符(Pipe-Forward Operator)

在应用.NET类库时,“|>”操作符很有用,因为它可以帮助编译器正确地推导出函数参数的类型。它的定义很简单:

F# Code
let (|>) x f = f x


类型信息为:

Tips
'a -> ('a -> 'b) -> 'b


可以这么来理解:x的类型为'a,函数f接受'a类型的参数,返回类型为'b,操作符的结果就是将x传递给f后所求得的值。除了这样将参数“转交”外,“|>”更重要的作用在于帮助编译器进行类型推导:

F# Code
open System
let dateList = [ new DateTime(1999, 9, 18);
new DateTime(2000, 9, 19);
new DateTime(2001, 9, 20) ]

List.iter (
fun d -> print_int d.Year) dateList


此时编译器会报告错误,因为它不能推导出d的类型(这个让我感到有点奇怪,iter函数的类型为(a’ -> unit) -> a’ list -> unit,它能推导出dateList的类型,却不能得出d的类型)。此时使用“|>”就没问题了,因为我们显式地告诉编译器d的类型:

F# Code
dateList |> List.iter (fun d -> print_int d.Year)


要了解事情的端倪,我们最好再来看第二个例子:

F# Code
type fsDate = { year : int; month : int; day : int }
let fsDateList =
[ { year =
1999; month = 12; day = 31 }
{ year =
2000; month = 12; day = 31 }
{ year =
2001; month = 12; day = 31 } ]

List.iter (
fun d -> print_int d.year) fsDateList
fsDateList
|> List.iter (fun d -> print_int d.year)


这段代码不会有编译错误,虽然看起来跟前一个例子很像,其主要区别在于fsDate是F#中的自定义类型,DateTime则是非F#类库中的类型。我们可以得出结论,不管是外部的.NET类型还是F#类型,“|>”都可使用,而F#的自动类型推导最好用在F#类型上

F# Code
let methods = System.AppDomain.CurrentDomain.GetAssemblies()
|> List.of_array
|> List.map (fun assm -> assm.GetTypes())
|> Array.concat
|> List.of_array
|> List.map (fun t -> t.GetMethods())
|> Array.concat

print_any methods


“|>”操作符还可用于串联多个函数调用,每次函数调用都将返回值传给下一个函数。

小结

走马观花,这一站的风景看得差不多了,命令式编程的核心部分也介绍完毕。有了函数式编程和命令式编程的知识,我们应该有信心解决大部分问题了。使用F#,我们可以选择合适的编程范式,而不是囿于特定的一种范式。下一站,我们将看到第三种主要的编程范式——面向对象编程。

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

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