F#探险之旅(三):命令式编程(下)(转)
控制流程(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事件
#light
open System.Timers
module WF = System.Windows.Forms
let timer =
let temp = new Timer()
temp.Interval <- 1000.0
temp.Enabled <- true
let messageNo = ref 0
temp.Elapsed.Add(fun _ ->
let messages = ["bet"; "this"; "gets";
"really"; "annoying"; "very"; "quickly";]
WF.MessageBox.Show(List.nth messages !messageNo) |> ignore
messageNo := (!messageNo + 1) % (List.length messages))
temp
print_endline "Whack the return to finish!"
read_line() |> ignore
timer.Enabled <- false
能附加事件处理器,当然也能移除事件处理器,这需要使用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