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事件


能附加事件处理器,当然也能移除事件处理器,这需要使用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


本文转自一个程序员的自省博客园博客,原文链接:http://www.cnblogs.com/anderslly/archive/2008/09/28/fs-adventure-ip-part-two.html,如需转载请自行联系原作者。

目录
相关文章
|
20天前
|
机器学习/深度学习 人工智能 算法
探索编程世界的奇妙之旅:从初学者到小有成就
【10月更文挑战第14天】探索编程世界的奇妙之旅:从初学者到小有成就
26 0
|
3月前
|
程序员 Python
探索代码之美:我的编程感悟之旅
【8月更文挑战第31天】编程,一门艺术与科学的结合体。本文将带你走进编程世界,分享个人在代码编写过程中的心得体会。从最初的迷茫到逐步掌握,再到深入理解,每一步都充满挑战与惊喜。让我们一起领略代码的魅力,感受编程带来的成就感与乐趣。
|
5月前
|
存储 人工智能 算法
计算机编程:从基础到实践,探索编程的奥秘
计算机编程:从基础到实践,探索编程的奥秘
58 1
|
6月前
|
设计模式 算法 测试技术
探索代码之美:我的编程思考之旅
【5月更文挑战第8天】 在数字化的浪潮中,编程已成为一种艺术,一种用逻辑与创造力编织的语言。本文将分享我在编程实践中的一些技术感悟,从最初的困惑到逐渐的深入理解,再到最后的灵活应用,我经历了一段充满挑战与收获的旅程。文章不仅探讨了编程技巧的提升,还涉及了对软件设计原则的认识,以及如何通过不断学习来适应快速变化的技术环境。
|
6月前
|
程序员
代码与禅意:编程中的悟性之旅
【5月更文挑战第31天】在数字世界的繁花似锦中,我们常常忽略了编码背后蕴含的哲学。本文将探讨编程不仅仅是一门技术,更是一种艺术和内省的过程。从禅宗的角度出发,我们将一窥那些静谧的代码行间所折射出的深邃智慧,以及它如何影响程序员的思考方式和解决问题的策略。
|
6月前
|
编译器
泛型编程的启蒙之旅
泛型编程的启蒙之旅
36 0
|
算法 Unix Java
初学者值得一看:什么是编程/C语言,编程学习建议,编程解疑与误区注意
初学者值得一看:什么是编程/C语言,编程学习建议,编程解疑与误区注意
176 0
|
JavaScript Java 编译器
编程简单科普系列 - 什么是编程 (1)
广义上来说,编程也不仅仅是指关于计算机的范畴,现在通常也指可以diy的东西,可以通过自己想法进行自定义的东西。当然我们这里所说的是指计算机编程. 因为计算机是不能直接听懂我们人说话的,没办法直接沟通,所以我们需要通过编程语言来跟计算机进行沟通,通过编程语言把我们想表达的转化为计算机能听懂的,在这里编程语言就相当一个翻译(比如,我们不会英文,准备要和一说英文的外国人谈生意,这时候就需要找一个英文翻译,替我们把想要表达的中文翻译成对方听得懂的英文。)其实严谨点这个比喻说也不准确,因为编程语言一般是需要通过编译器或者解释器编译解释之后,才能为计算机所识别. 每个计算机编程语言,都有自己的编译器或者解
352 0
初识“方法”,步入编程领域 | 带你学《Java编程入门》之六
在程序之中很多情况下是有可能需要重复执行一些代码的。在有一些书上也会把方法(method)称为函数(function)。需要注意一点,本次进行方法定义的时候有一个前提:方法在主类中定义,并且由主方法直接调用。
初识“方法”,步入编程领域   | 带你学《Java编程入门》之六