第四章 命令编程(二)

第四章    命令编程(二)



控制流(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)。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值