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

  《F# Specs

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值