第四章 命令编程(二)
控制流(control flow)
不像在第三章中描述的伪控制流(pseudo-control-flow),F# 还有一些命令式控制流结构。不权有if 命令,还有while 和for 循环。
与命令式风格中使用if 表达式的主要不同在于,在函数中使用if,返回空类型,且不强制使用else。如下面的例子所示:
if System.DateTime.Now.DayOfWeek =System.DayOfWeek.Sunday then
printfn "Sunday Playlist: Lazy On ASunday Afternoon - Queen"
虽然如果if 表达式返回空类型,else 表达式就不是必须的;但如果需要,也可以加一个,但也必须是空类型,否则,会编译出错:
if System.DateTime.Now.DayOfWeek =System.DayOfWeek.Monday then
printfn "Monday Playlist: Blue Monday -New Order"
else
printfn "Alt Playlist: Fell In Love WithA Girl - White Stripes"
可以用空白去决定if 表达式的结尾。缩进的代码属于同这个 if 表达式,if 表达式结束时缩进回到原始位置。下面的例子中,字符串 "Tuesday Playlist: Ruby Tuesday – RollingStones" 只在星期二才输出,而"Everyday Playlist: Eight Days A Week - Beatles" 每天都会输出。
if System.DateTime.Now.DayOfWeek =System.DayOfWeek.Tuesday then
printfn "Tuesday Playlist: Ruby Tuesday -Rolling Stones"
printfn "Everyday Playlist: Eight DaysA Week - Beatles"
如果想让多个语句成为if 语句的一部分,只要让它们的缩进相同。下面的例子中两个字符串,只在在星期五才会输出:
if System.DateTime.Now.DayOfWeek =System.DayOfWeek.Friday then
printfn "Friday Playlist: Friday I'm InLove - The Cure"
printfn "Friday Playlist: View From TheAfternoon - Arctic Monkeys"
大多数程序员都很熟悉for 循环,是因为它在命令编程语言很常见。在 F# 中,for 循环被重载,因此,for 循环既可以枚举集合,其行为类似于许多编程语言中的 foreach 循环,也可以指定标识符,每一次循环之后,其值加1。
首先,我们来看一下用 for 枚举集合手情况,这里的 for 循环执行命令动作,针对集体中的每一个元素,返回空,这可能是 F# 中 for 循环的最常规用法。其语法为,关键字 for,加标识符,用于到集合中的每一项,加关键字 in,加集合,再加关键字 do;接下来是处理集合中每一项的代码,需要缩进,表示代码属于这个 for 循环。下面的示例演示枚举一个字符串数组,并打印其中的每一项:
// an array for words
let words = [| "Red";"Lorry"; "Yellow"; "Lorry" |]
// use a for loop to print each element
for word in words do
printfn "%s" word
运行结果如下:
Red
Lorry
Yellow
Lorry
我们将会看到,在本章的后面,以及本书中,这个语法可能是处理由 .NET BCL 方法返回的、类型化或非类型化集合,最方便的方法。
for 循环的其他用法,需要声明标识符,其作用域只在这个 for 循环,每一次循环后,这个值加 1 或减 1。这个标识符给定一个起始值,一个终止值,终止值提供了循环终止的条件。F# 沿用这个语法,以关键字for开始,加用于计数的标识符,加等号,加初始计数的表达式,加关键字to,加终止值的表达式;之后是构成循环体的代码,夹在关键字do和done之间[ 关键字done 可以省略]。for 循环返回空类型,因此,构成循环体的代码也应该是pwa 类型,否则,编译器会警告。
下面的例子演示了for 循环的常规用法:枚举数组中所有值。标识符index 的初始值为0,终止值为数组长度- 1,这个标识符可以用作数组的索引。
// a Ryunosuke Akutagawa haiku array
let ryunosukeAkutagawa = [| "Green"; "frog,";
"Is"; "your"; "body"; "also";
"freshly";"painted?" |]
// for loop over the array printing eachelement
for index = 0 to Array.lengthryunosukeAkutagawa - 1 do
printf"%s " ryunosukeAkutagawa.[index]
示例的运行结果如下:
Green frog, Is your body also freshlypainted?
在常规的for 循环中,初始计数值必须总是小于终止值,每次循环后计数值会增加。有一个变化,把to 被换成downto,这样,初始计数值必须总是大于终止值,每次循环后计数值将减少。下面的例子演示如何使用downto:
// a Shuson Kato hiaku array (backwards)
let shusonKato = [| "watching.";"been "; "have ";
"children "; "three ";"my "; "realize "; "and ";
"ant "; "an "; "kill"; "I ";
|]
// loop over the array backwards printingeach word
for index = Array.length shusonKato - 1downto 0 do
printf "%s " shusonKato.[index]
示例的运行结果如下:
I kill an ant and realize my three childrenhave been watching.
while 循环是另一个常用的命令语言构造,它是在一段代码上创建循环的表达式,直到逻辑表达式的值变成false。在F# 中创建while 循环,使用关键字while,加决定循环是否应该继续的逻辑表达式。像for 循环一样,把循环体放在关键字do 和done 之间[ done可以省略],循环体的类型应该空,否则,编译器会警告。下面代码演示了while 循环的例子:
// a Matsuo Basho hiaku in a list reference
let matsuoBasho = ref [ "An";"old"; "pond!";
"A"; "frog"; "jumps"; "in-";
"The"; "sound"; "of"; "water" ]
while (List.length !matsuoBasho > 0) do
printf "%s " (List.head !matsuoBasho)
matsuoBasho := List.tail !matsuoBasho
程序枚举一个列表,逻辑表达式根据列表是否为空决定终止循环。在循环体内,输出列表的头,然后删除,每一次循环,列表都在缩短。
下面是运行的结果:
An old pond! A frog jumps in- The sound ofwater
调用.NET 库中的静态方法和属性
在F# 中进行命令编程,能够尽可能多地使用.NET 编程语言编写的库函数,包括许多 BCL 本身的方法和类,是非常有用。我认为这才是命令编程,因为用其他语言写的库函数,不能保证在其中的工作状态如何,因此,就不可能知道调用方法是否有副作用。
调用以F# 编写的和其他语言编写的库函数是有区别的。用F# 编写的库函数有描述这个库函数额外细节的元数据,比如,方法的参数是元组,还是可散的(curried)。元数据 F# 是专有的,以二进制的形式给存储,成为产生的程序集的资源。这就是为什么Microsoft.FSharp.Reflection 提供了大量的 API,成为 F# 与 .NET 元数据之间的桥梁。
调用静态的或实例的属性或方法,其基本语法是相同的。调用非F# 库函数的方法,参数必须用逗号隔开,并用括号括起来。(记住,F# 的函数调用通常使用空格分隔参数,不需要使用括号,除非为了改变优先级。)调用非F# 库函数的方法不能散(curried),事实上,非 F# 库函数方法的参数相当于元组。尽管有这些不同,但调用非F# 库函数的方法仍然非常简单。我们先从使用静态属性和方法开始:
open System.IO
// test whether a file "test.txt"exist
if File.Exists("test.txt") then
printfn "Text file \"test.txt\" is present"
else
printfn "Text file \"test.txt\" does not exist"
这个例子调用了.NET 框架BCL 中的一个静态方法。调用静态方法与调用F# 函数几乎一样,开始是类名,加点,再加方法名;在语法上真正的不同是传递参数的不同,参数用括号括起来,并用逗号分隔。这里,调用System.IO.File 类的Exists 方法检测一个文件是否存在,然后依据结果输出对应的消息。
我们可以把 F# 的函数看作是值,同样,其他 .NET 库函数中的静态方法也可以看作是值,可以作为参数传递给其他函数。在下面的示例中,我们把 File.Exist 方法传递给 F# 的库函数 List.map:
open System.IO
// list of files to test
let files1 = [ "test1.txt";"test2.txt"; "test3.txt" ]
// test if each file exists
let results1 = List.map File.Exists files1
// print the results
printfn "%A" results1
因为 .NET 方法从行为上看,就好像是取元组作为参数,因此,就可以把有多个参数的方法看作是一个值。下面,我们会看到如何把File.WriteAllBytes 应用到一个元组列表,这个元组包含了文件的路径(字符串),和期望的文件内容(字符数组):
open System.IO
// list of files names and desired contents
let files2 = [ "test1.bin", [|0uy |];
"test2.bin",[| 1uy |];
"test3.bin",[| 1uy; 2uy |]]
// iterator over the list of files creatingeach one
List.iter File.WriteAllBytes files2
通常,我们不仅打算使用 .NET 方法中已经的功能,还想能有散(curry)的能力。在 F# 中,通常的模式是,用 F# 写一个很薄的封装函数(wrapper),导入 .NET 方法的功能。看下面的示例:
open System.IO
// import the File.Create function
let create size name =
File.Create(name,size, FileOptions.Encrypted)
// list of files to be created
let names = [ "test1.bin";"test2.bin"; "test3.bin" ]
// open the files create a list of streams
let streams = List.map (create 1024) names
这里,我们看到了如何导入 File.Create,重载了取三个参数的方法,只暴露了其中的两个作为参数:缓冲的大小(size)和文件名(name)。注意,我们是如何把 size 指定为第一个参数的,这样做的原因是,当我们准备产生几个文件时,使用相同缓冲的可能性要大于使用相同文件名的可能性。在程序清单的最后一行,我们把create 函数应用到一个文件名的列表,创建一个文件流列表。如果想让妴的每一个流的缓冲大小为 1024 字节,就传递文字 1024 给create 函数,像这样:(create 1024)。它返回一个新函数,给 List.map 函数。
当使用的.NET 方法有大量参数时,能知道参数名是很有帮助的,可以跟踪每个参数在干什么。F# 可以使用命名参数,给定参数的名字,加等号,再参数的值。下面的例子演示重载有四个参数的 File.Open() 的方法:
open System.IO
// open a file using named arguments
let file = File.Open(path ="test.txt",
mode = FileMode.Append,
access = FileAccess.Write,
share = FileShare.None)
// close it!
file.Close()
使用.NET 库中的对象和实例成员
使用非F# 库函数中的类也很简单。实例化对象的语法为,关键字new,加需要实例化的类名,再加放在括号中、用逗号隔开的构造函数的参数。可以用关键字let,把类的实例绑定到标识符。对象一旦和标识符关联,其行为就很像记录类型;不能修改被引用的对象,但可以修改它的内容。另外,如果这个标识符不在顶级,就可以重新定义,或者在其他作用域中被一个同名的标识符所隐藏。C# 和VB 程序员会发现,访问字段、属性、事件和方法相当直观的,因为语法是相似的。为了访问任意成员,用对象的标识符,加点,再加成员名。
实例方法的参数与静态方法相同,必须放在括号中,用逗号隔开。读取属性、字段的值,只需要成员名;要修改,使用左箭头(<-)。
下面的例子演示如何创建System.IO.FileInfo 对象,然后,用这个类的多个成员,以不同的方式去操作。第一行,在 F# 中启用System.IO 命名空间;第二行,创建FileInfo 对象,并把需要的文件名作为参数传递给它;下一行,用实例属性Exists 检查文件是否存在,如果文件不存在,用实例方法CreateText()创建一个新文件,并用实例属性Attributes 把文件置为只读。这里,还使用了use绑定,这样,当标识符超出作用域,会调用 Dispose 方法,清除占用的资源:
open System.IO
// create a FileInfo object
let file = newFileInfo("test.txt")
// test if the file exists,
// if not create a file
if not file.Exists then
usestream = file.CreateText()
stream.WriteLine("hello world")
file.Attributes <- FileAttributes.ReadOnly
// print the full file name
printfn "%s" file.FullName
这一点,我们在第三章中已经充分讨论过,另外,F# 还可以在构造对象同时设置属性。在设置属性作为初始化配置对象进程的一部分是相当普遍的,特别是在WinForms 编程中(有关WinForms 在第八章中有更多的介绍)。在构造时设置属性,在构造函数中放属性名,加等号,再加属性值;多个属性用逗号隔开。下面的程序是前面示例的变化,设置只读属性放在了对象构造时:
(但是,功能上是有所不同,文件必须已经存在;如果不存在,啥也不做。)
open System.IO
// file name to test
let filename = "test.txt"
// bind file to an option type, dependingon whether
// the file exist or not
let file =
ifFile.Exists(filename) then
Some(new FileInfo(filename, Attributes = FileAttributes.ReadOnly))
else
None
注意,当尝试设置文件的 Attributes 属性时,要检测文件是否存在,以避免运行时的异常。F# 可以在调用构造函数的同时设置类型参数,因为,在构造函数调用时,并不一定能推断出类型参数。类型参数放在尖括号(<>)中,用逗号分隔。下面的例子演示如何在调用构造函数时设置类型参数,创建System.Collections.Generic.List 的实例,在实例创建时,通过设置类型参数,使列表只能用整数。在 F# 中,System.Collections.Generic.List 被称为可变大小的数组(ResizeArray),以避免与 F# 中的列表混淆。
open System
// an integer list
let intList =
let temp = new ResizeArray<int>() in
temp.AddRange([| 1 ; 2 ; 3 |]);
temp
// print each int using the ForEach membermethod
intList.ForEach( fun i ->Console.WriteLine(i) )
结果如下:
1
2
3
前面的例子还演示了 F# 与非 F# 库函数互操作中另一个非常好的功能。.NET API 通常用一种称为委托(delegate)的.NET 构造,在概念上,它是一种函数值。如果签名(signature)相匹配,F# 函数将自动转换成.NET 的委托对象。在最后一行,可以看到F# 函数直接传递给需要.NET 委托类型做参数的方法。
为了保持方法尽可能灵活,在导入需要泛型委托做参数的方法时,或者为非F# 库函数的构造函数创建一个F# 封装函数时,最好不要指定类型参数,用下划线替代类型参数就可以了,如下面示例中第一行代码所示(下面例子使用了向前管道运算符|>,会有专门一节介绍):
open System
// how to wrap a method that take adelegate with an F# function
let findIndex f arr = Array.FindIndex(arr,new Predicate<_>(f))
// define an array literal
let rhyme = [| "The";"cat"; "sat"; "on"; "the";"mat" |]
// print index of the first word ending in'at'
printfn "First word ending in 'at' inthe array: %i"
(rhyme |> findIndex (fun w -> w.EndsWith("at")))
运行结果如下:
[|"The"; "cat";"sat"; "on"; "the"; "mat"|]
First word ending in 'at' in the array: 1
这里,从System.Array 类中导入FindIndex 方法,因此,能够以散(curried)风格使用。如果不显式创建委托,标识符 f 就可能表现就是一个 Predicate 委托,而不是函数,就是说,对findIndex 的所有调用,都需要显式创建委托对象,这就不完美了。
[
let findIndex f arr = Array.FindIndex(arr,new Predicate<_>(f))
val findIndex : f:('a -> bool) ->arr:'a [] -> int
let findIndex f arr = Array.FindIndex(arr,f)
val findIndex : f:Predicate<'a> ->arr:'a [] -> int
]
然而,在创建 Predicate 委托时,如果在findIndex 的定义中指定类型[new Predicate<_> ],就会限制函数 findIndex 只用于指定类型的数组。有时,这可能是你想要做的,但不是通常情况。通过用下划线,避免为findIndex 指定类型,保持优雅且灵活。
使用.NET 库中的索引器(indexer)
索引器是一个.NET 概念,使类的集合看起来更象数组。索引器是一个专有属性,总是被称为项(item),有一个或多个参数。方便访问索引器的属性是很重要的,因为 BCL 中的许多类都有索引器。
F# 提供两种不同的语法访问索引器。既可以显式使用 item 属性,也可以使用类似数组的语法,但用方括号([])代替括号(())。
open System.Collections.Generic
// create a ResizeArray
let stringList =
lettemp = new ResizeArray<string>() in
temp.AddRange([| "one"; "two"; "three"|]);
temp
// unpack items from the resize array
let itemOne = stringList.Item(0)
let itemTwo = stringList.[1]
// print the unpacked items
printfn "%s %s" itemOne itemTwo
示例将字符串"one" 和 "two" 分别与标识符 itemOne 和itemTwo 关联。"one" 和 itemOne 关联,演示了显式使用 item 属性;"two" 和 itemTwo 关联,使用的是方括号语法。
注意:这个例子还演示了F# 中一个常用模式,如何把标识符 stringList 创建成非F# 库函数的对象,同时,初始化为特定值。具体做法是,把这个对象分配给一个临时的标识符;然后,调用这个对象的一个实例成员去操作它的内容;最后,返回这个临时标识符,这样,它就变成了 stringList 的值。用这种方法,可以保持对象的创建与初始化在一起。
使用.NET 库中的事件(event)
事件是对象的专有属性,可以为事件附加函数。附加给事件的函数有时也称为[ 事件]处理程序(handler)。当事件发生时,会执行所有附加给它的函数。例如,创建按钮(Button)对象,暴露它的单击(Click)事件,那么,当用户单击按钮时事件就会发生。就是说,当按钮被单击时,附加给按钮单击事件的所有函数都会执行。这是相当有用的,因为当创建用户界面时,需要通告用户到底做了什么,这是相当普遍。
添加事件处理程序十分简单。每个事件都会有一个方法Add,把需要处理的事件传递给这个方法。由于事件来自非 F# 库函数,因此,Add 方法也需要符合参数必须放在括号中的给定。在 F# 中,通常把处理函数就放在 Add 方法中,使用 F# 匿名函数的功能。处理函数的类型必须匹配 Add 方法参数的类型,这个参数的类型为'a -> unit。就是说,对于 BCL 中对象提供的事件,Add 方法参数的类型将相似于EventArgs->Unit。
下面的例子演示创建计时器(Timer)对象,并为计时器的Elapsed 事件添加一个函数。计时器对象按照一定的间隔,会触发 Elapsed 事件。这里,处理程序显示一个消息框,通知用户。注意,不必关心传递给处理函数的参数,因此,用下划线忽略。
open System.Windows.Forms
open System.Timers
let timer =
//define the timer
lettemp = new Timer(Interval = 3000.0,
Enabled = true)
// a counter to hold the current message
let messageNo = ref 0
// the messages to be shown
let messages = [ "bet";"this"; "gets";
"really"; "annoying";
"very"; "quickly" ]
// add an event to the timer
temp.Elapsed.Add(fun _ ->
//show the message box
MessageBox.Show(List.nth messages !messageNo) |> ignore
//update the message counter
messageNo := (!messageNo + 1) % (List.length messages))
// return the timer to the top level
temp
// print a message then wait for a useraction
printfn "Whack the return tofinish!"
System.Console.ReadLine() |> ignore
timer.Enabled <- false
注意
如果要编译这个程序,需要添加对System.Windows.Forms.dll 程序集的引用,有了它,才可以访问 System.Windows.Forms命名空间。
也可以从事件中删除处理程序。要这样做,必须要保持添加的函数在作用域内;这样,就可以把它传递给事件的 RemoveHandler 方法。RemoveHandler 方法接受委托,它是封装了一个正常.NET 方法的对象,可以象值一样传递。就是说,为事件指定的处理函数,必须已经封装到委托中,因此,必须用事件的AddHandler(或 Removehandler)方法,而不是Add(或 Remove)方法。在 F# 中创建委托非常简单,只需要调用委托的构造函数,其方法与调用任何非 F# 库函数对象的构造函数相同,把它传递给委托封装的函数:
open System
open System.Windows.Forms
// define a form
let form =
//the temporary form defintion
lettemp = new Form(Text = "Events example")
//define an event handler
letstuff _ _ = MessageBox.Show("This is \"Doing Stuff\"")|> ignore
letstuffHandler = new EventHandler(stuff)
//define a button and the event handler
letevent = new Button(Text = "Do Stuff", Left = 8, Top = 40, Width = 80)
event.Click.AddHandler(stuffHandler)
//label to show the event status
letlabel = new Label(Top = 8, Left = 96)
//bool to hold the event status and function
//to print the event status to the label
leteventAdded = ref true
letsetText b = label.Text <- (Printf.sprintf "Event is on: %b" !b)
setText eventAdded
//define a second button and it's click event handler
lettoggle = new Button(Text = "Toggle Event",
Left = 8, Top = 8, Width= 80)
toggle.Click.Add(fun_ ->
if!eventAdded then
event.Click.RemoveHandler(stuffHandler)
else
event.Click.AddHandler(stuffHandler)
eventAdded:= not !eventAdded
setTexteventAdded)
//add the controls to the form
letdc c = (c :> Control)
temp.Controls.AddRange([|dc toggle; dc event; dc label; |])
//return the form to the top level
temp
// start the event loop and show the form
do Application.Run(form)
这个示例演示了如何在 F# 中创建一个简单的Win 窗体应用程序。事件与用户界面编程同义,因此,我认为能够展示在这方面使用的事件中的一个示例,是一个好方法。在示例的开头,创建委托StuffHandler;然后,把它添加到按钮 event(怎么起这么个名字,太容易混淆了)的单击(Click)事件;再后来,直接把处理程序添加按钮toggle 的单击事件,它实现了在按钮的事件中添加、删除处理程序。
警告:
前面的示例不能运行在 F# 的交互控制台fsi 中,因为调用 Application.Run;如果要使用fsi,应该用 form.Visible <- true;; 替换。
.NET 类型的模式匹配
在第三章中,我们已经见识过F# 中模式匹配非常强大的功能。模式匹配可以让程序员根据匹配的值来决定不同的计算。F# 有一个能在 .NET 类型上进行模式匹配的结构。匹配 .NET 类型的规则是这样构成的,冒号加问题运算符(:?),加准备匹配的 .NET 类型名。因为不可能完全列出所有的 .NET 类型,因此,在模式匹配 .NET 类型时,必须提供一个默认规则。
// a list of objects
let simpleList = [ box 1; box 2.0; box"three" ]
// a function that pattern matches over the
// type of the object it is passed
let recognizeType (item : obj) =
match item with
|:? System.Int32 -> printfn "An integer"
|:? System.Double -> printfn "A double"
|:? System.String -> printfn "A string"
| _-> printfn "Unknown type"
// iterate over the list of objects
List.iter recognizeType simpleList
运行结果如下:
An integer
A double
A string
这个例子定义了函数 recognizeType,通过模式匹配识别三种 .NET 基本类型;然后,将这个函数应用于一个列表。这个函数中有两个细节值得关注:第一,函数接收 obj 类型的参数,需要使用类型注释。如果不使用类型注释,那么编译器会推断该函数可以接收任意类型,即类型 'a ,这会引起问题,因为不能对 F# 的类型使用这种类型的模式匹配,它只能用在 .NET 类型上;第二,函数的默认情况使用下划线忽略。
一旦把值识别成特定类型,通常情况下,还会想用这个值来做点什么。为此,在规则右边用关键字 as,加标识符。在下面的例子会看到重写了的 recognizeType,增加了这个功能,当类型识别以后,在消息中输出值。
// list of objects
let anotherList = [ box "one";box 2; box 3.0 ]
// pattern match and print value
let recognizeAndPrintType (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
// interate over the list pattern matchingeach item
List.iter recognizeAndPrintType anotherList
运行结果如下:
A string: one
An integer: 2
A double: 3.000000
注意最后的默认规则,只有一个标识符,由于已经知道它是 obj 类型,因此,不需要进行类型匹配,同已经匹配的值一样,已经是 obj 类型。
针对 .NET 类型的模式匹配,对于处理由 .NET 方法引起的异常,也非常有用。构成模式匹配的方法相同,只要把try … match 构造替换成try … with。下面的例子演示了如何匹配和捕获两个.NET 异常,匹配到引发的这,根据异常的类型,输出不同的消息到控制台。
try
//look at current time and raise an exception
//based on whether the second is a multiple of 3
ifSystem.DateTime.Now.Second % 3 = 0 then
raise (new System.Exception())
else
raise (new System.ApplicationException())
with
| :? System.ApplicationException ->
//this will handle "ApplicationException" case
printfn "A second that was not a multiple of 3"
| _ ->
//this will handle all other exceptions
printfn "A second that was a multiple of 3"
向前管道(|>)运算符
向前管道运算符,在第三章组合函数(函数应用)一节我们已经碰到过,用它可以把值传递给函数,但是,函数与参数的位置同源文件中正常出现的顺序相反。快速回忆一下,下面的示例就是这个运算符的定义和用法:
// the definition of the pipe-forwardoperator
let (|>) x f = f x
// pipe the parameter 0.5 to the sinfunction
let result = 0.5 |> System.Math.Sin
这项技巧在处理 .NET 库时特别有用,因为,它可以帮助编译器准确推断出函数参数的类型,而不需要显式的类型注释。
为了更好地理解这个运算符的用处,有必要深入了解类型推断是如何从右到左进行的。下面的例子定义了一个整型列表intList,类型为int list;然后,把这个列表传递给库函数 List.iter,作为第二个参数,第一个参数是函数,类型为 int->unit。
let intList = [ 1; 2; 3 ]
// val printInt: int list
let printInt = printf "%i"
// val printInt: int -> unit
List.iter printInt intList
现在,需要理解的是程序中的这些表达式是如何分配给它们的类型的。编译器首先从输入文件的开头找到标识符 intList,从绑定到它上面的文字来推断类型;然后,找到标识符printInt,推断出它的类型为 int->unit,因为它就是调用printf 函数返回的类型;接着,找到函数 List.iter,已知它的类型是 ('a -> unit) -> 'a list -> unit。因为,其中有泛型或待定类型 'a,编译器必须向左检查下一个标识符,这里是函数 printInt,函数的类型是 int->unit,因此,编译器推断泛型参数 'a 的类型为 int,那么,传递给函数的列表类型必须是 int list。
因此,确定的列表的类型,就是函数类型。然而,从应用的列表的类型推断函数的类型,通常是有用的,特别是在处理.NET 类型的时候,能够不需要类型注释就可以访问成员。向前管道运算符是通过把列表放在处理它的函数前面实现的。看下面的示例:
open System
// a date list
let importantDates = [ newDateTime(1066,10,14);
newDateTime(1999,01,01);
newDateTime(2999,12,31) ]
// printing function
let printInt = printf "%i "
// case 1: type annotation required
List.iter (fun (d: DateTime) -> printIntd.Year) importantDates
// case 2: no type annotation required
importantDates |> List.iter (fun d ->printInt d.Year)
这里,有两种方法打印日期列表中的年。第一种情况,需要添加类型注释才能访问 DateTime 结构的方法和属性。之所以类型注释是必须的,是因为,这个时候编译器还没有遇到importantDates,没有足够的信息来推断匿名函数中参数 d 的类型;而在第二种情况中,列表importantDates 放在前面,就有足够的信息推断出d 的类型。
向前管道运算符还可以用于把几个函数链接在一起,即一个函数处理另一个函数的结果。看下面的示例,我们首先获得在内存中运行的所有 .NET 程序集;然后,处理这个列表;最后,得到在内存中的所有 .NET 方法。因为,每一个函数处理前一个函数的结果,向前管道运算符能够用于显示结果,成为管理并向前传递给下一个函数,不需要声明中间变量来保存中间的结果:
// grab a list of all methods in memory
let methods =System.AppDomain.CurrentDomain.GetAssemblies()
|>List.ofArray
|>List.map ( fun assm -> assm.GetTypes() )
|>Array.concat
|>List.ofArray
|>List.map ( fun t -> t.GetMethods() )
|>Array.concat
// print the list
printfn "%A" methods
我们会发现,这是一项非常有用的技巧,在本书的后面还会时常出现。
第四章小结
在这一章中,我们学习了F# 的命令式功能,加上第三章讨论的函数式功能,我们已经有了攻克任何计算难题的所有能力,F# 给了我们在任何需要的时候选择适当范式并组合范式的能力。下一章,我们将学习F# 的第三个编程范式,面向对象编程(object-oriented programming)。