什么是F#,我为何要学它?
F#是一种.NET平台上的函数式编程语言。就像C#和VB.NET,F#可以利用.NET的核心类库,如WPF,WCF,VSTO等等,通过F#您甚至可以使用XNA编写XBox游戏。
仅仅如此并不意味着您应该去学习它。那为何要使用F#呢?作为一种函数式编程语言,F#使得某些领域的编程要比命令式编程(如使用C#)更为容易。并行编程(Parallel Programming)和面向语言编程(Language-Oriented Programming)是其中的两个领域。
如果您曾经编写过.NET应用程序,并感觉自己在努力使用手头的语言表达自己的想法,也许F#就是您在寻找的。
上路
首先得下载(F#的最新版本1.9.4.19)和安装。安装程序会在VS2005和VS2008中安装F#的项目系统(项目模板和项模板)。先来创建一个新的F#项目。
然后添加一个新的F#源文件file1.fs。默认条件下,新建的源文件包含了很多“教学”代码,全部删除,然后输入下面的代码:
#light
let square x = x * x
let numbers = [1 .. 10]
let squares = List.map square numbers
printfn "N^2 = %A" squares
open System
Console.ReadKey(true)
按下F5运行程序,您会看到:
这些并没有太多让人兴奋的。我们来逐行的分析下代码,看看到底有什么不同之处,在此之前先介绍下VFSI。
Visual Studio中的F#交互(Interactive)
F#交互控制台(F# Interactive Console, FSI)采用的是“REPL loop”模式,即Read-Evaluate-Print-Loop。也就是输入一段代码,编译并执行,然后输出结果。通过它您可以快速地开发和测试程序。要在VS中启用FSI,打开Add-in Manager窗口。
选中“F# Inactive for Visual Studio”。然后,选中程序的前两行代码:
接着按下Alt+Enter(实际上,如果FSI还么有打开,需要按两次Alt+Enter)。这时会看到出现了一个工具窗口:
我们刚才做的事情是将代码片段直接发送给FSI会话,FSI将结果输出。结果是函数“square”的定义,它接受int类型参数,返回类型也是int。
接下来在FSI窗口输入“List.map square [1 .. 2 .. 10];;”。“;;”是告诉FSI停止阅读程序,立即进行求值。
> List.map square [1 .. 2 .. 10];;
val it : int list = [1; 9; 25; 49; 81]
现在我们可以方便地通过FSI来学习F#了,马上来看看我们的程序究竟做了什么吧。不过仍建议您在VS源代码编辑器中输入代码,使用“Select(原文是Highlight,感觉Select更贴切) + Alt + Enter”将代码片段发送至FSI。
语言基础
#light(OCaml兼容)
F#源自OCaml,具有交互编译OCaml的能力,也就是可以不经修改即可编译简单的OCaml程序。这种能力也带来了令人讨厌的语法。#light(发音为hash-light)是一个编译器指令,可以简化F#的语法。
强烈建议您保持使用#light,您会发现,在大多数F#代码片段中要么会声明它,要么是假定已经声明了它。
let square x = x * x(类型推演)
这行代码定义了一个函数:square,它会求得数字x的平方。考虑一下C#中等价的代码:
public static int square(int x)
{
return x * x;
}
在C#中,您需要制定参数和返回值的类型信息,而F#则帮您搞定了。这种行为称为类型推演(Type Inference)。
从函数的签名,F#可以知道“square”函数接受一个参数“x”,并且函数返回“x * x”(在函数体内的最后一次求值将作为返回值,因此无须return关键字)。因为很多基元类型都支持*操作,比如byte,uint64,double等,F#默认会使用int类型,有符号的32位整数。
现在考虑下面的代码,它为其中的一个参数提供了“类型注解(type annotation)”,告诉编译器期望的类型。因为x标为“string”,“+”操作只定义在两个string间,因此y也必须为string类型,返回值是两个字符串拼接的结果。
> let concat (x : string) y = x + y;;
val concat : string -> string -> string
> concat "Hello, " "World!";;
val it : string = "Hello, World!"
后面我们将讨论类型推演的更多高级主题,现在您只要享受F#编译器的智能带来的方便就好了。
let numbers = [1 .. 10](F# lists)
这行代码声明了一个列表(list),其元素是从1至10。如果您用的是[|1 .. 10|],F#会创建一个.NET的整型数组(array)。而在F#中,列表是一个不可变的链表(linked list),这也是函数式编程的基础。试着将这些代码输入到FSI中(记住添加“;;”):
// Define a list
let vowels = ['e'; 'i'; 'o'; 'u']
// Attach item to front (cons)
let cons = 'a' :: vowels
// Concat two lists
let sometimes = vowels @ ['y']
我将在本系列的第二篇中更深入地介绍列表。
let squares = List.map square numbers
现在我们有了一个整型列表(numbers)和一个函数(square),我们希望创建一个新的列表,它的每一项是对numbers的每一项进行square运算后的结果。
幸运的是,List.map可以做到。考虑下面的例子:
> List.map (fun x -> x % 2 = 0) [1 .. 10];;
val it : bool list
= [false; true; false; true; false; true; false; true; false; true]
代码(fun x -> x % 2 = 0)定义了一个匿名函数,称为lamdba表达式,接受一个参数x,返回值为表达式“x % 2 = 0”的结果,也就是判断x是否为偶数。
注意我们刚才做的——将一个函数作为参数传递给另一个函数。在C#中这个并不容易。但在F#可以很清楚地表达出来,而且代码很简洁。将函数像值一样传递被称为“一等函数(first order functions)”,也是函数式编程的基础。
printfn "N^2 = %A" squares
printf是打印文本到控制台窗口的一种简单而又类型安全的方式。要更好地了解printf,考虑下面的例子,它打印一个整数、浮点数和字符串。
> printfn "%d * %f = %s" 5 0.75 ((5.0 * 0.75).ToString());;
5 * 0.750000 = 3.75
val it : unit = ()
%d,%f,%s分别是int、float、string的占位符。%A则可用于打印任何值。
Console.ReadKey(true) (.NET互操作)
我们程序的最后一行只是简单地调用了System.Console.ReadKey方法,这样可以让程序在关闭其暂停。因为F#建立在.NET的基础上,您可以在F#中调用任何.NET类库——从正则表达式到WinForms。代码“open System”用于打开命名空间,类似于C#中的using。
现在我们已经有了F#的基础知识,可以继续学习更有趣的基础类型和F#概念了,希望您关注第二篇文章!
原文链接:http://blogs.msdn.com/chrsmith/archive/2008/05/02/f-in-20-minutes-part-i.aspx。
1.不可变性(Immutability)
您也许已经注意到,我一直使用“值(value)”来表示一个标识符(identifier),而不是“变量(variable)”。这是由于默认情况下,F#中的类型是不可变的(immutable),也就是说,一经创建即不可修改。看起来这是一个很大的限制,但是不可变性可以避免某种类型的bug。另外,不可变的数据天然地具备线程安全的特性,这意味着您无需在处理并行情况时担心同步锁的发生。我将在系列的第三篇中介绍异步编程。
如果您确实需要修改数据,可使用F#的mutable关键字,它会创建一个变量(而不是值)。我们可以通过左箭头操作符(<-)来修改变量的值。
> let mutable x = "the original value.";;
val mutable x : string
> printfn "x's value is '%s'" x;;
x's value is 'the original value.'
val it : unit = ()
> x <- "the new one.";;
val it : unit = ()
> printfn "x's value is now '%s'" x;;
x's value is now 'the new one.'
val it : unit = ()
2. 引用值(Reference values,Microsoft.FSharp.Core.Ref<_>)
引用值是另一种表示可修改数据的方式。但它不是将变量存储在堆栈(stack),引用值其实是一个指向存储在堆(heap)上的变量的指针(pointer)。在F#中使用可修改的值时会有些限制(比如不可以在内部lambda表达式中使用)。而ref对象则可被安全地传递,因为它们是不可变的record值(只是它有一个可修改的字段)。
使用引用值时,用“:=”赋一个新值,使用“!”进行解引用。
> let refCell = ref 42;;
val refCell : int ref
> refCell := -1;;
val it : unit = ()
> !refCell;;
val it : int = –1
3. 模块(Modules)
在前面中,我只是随意地声明了几个值和函数。您也许会问,“要把它们放在哪里呢?”,因为在C#中所有一切都要属于相应的类。尽管在F#中,我们仍然可以用熟悉的方式声明标准的.NET类,但它也有模块的概念,模块是值、函数和类型的集合(可以对比一下命名空间,后者只能包含类型)。
这也是我们能够访问“List.map”的原因。在F#库(FSharp.Core.dll)中,有一个名为“List”的模块,它包含了函数“map”。
在快速开发的过程中,如果不需要花费时间去设计严格的面向对象类型体系,就可以采用模块来封装代码。要声明自己的模块,要使用module关键字。在下面的例子中,我们将为模块添加一个可修改的变量,该变量也是一个全局变量。
module ProgramSettings =
let version = "1.0.0.0"
let debugMode = ref false
module MyProgram =
do printfn "Version %s" ProgramSettings.version
open ProgramSettings
debugMode := true
4. 元组(Tuples)
元组(tuple,发音为‘two-pull’)表示值的有序集合,而这些值可看作一个整体。按传统的方式,如果您要传递一组相关的值,需要创建结构(struct)或类(class),或者还需要“out”参数。使用元组我们可以将相关的值组织起来,同时并不需要引入新的类型。
要定义一个元组,只要将一组值用逗号分隔,并用圆括号把它们括起来即可。
> let tuple = (1, false, "text");;
val tuple : int * bool * string
> let getNumberInfo (x : int) = (x, x.ToString(), x * x);;
val getNumberInfo : int -> int * string * int
> getNumberInfo 42;;
val it : int * string * int = (42, "42", 1764)
函数甚至可以接受元组为参数:
> let printBlogInfo (owner, title, url) = printfn "%s's blog [%s] is online at '%s'" owner title url;;
val printBlogInfo : string * string * string -> unit
> let myBlog = ("Chris", "Completely Unique View", "http://blogs.msdn.com/chrsmith");;
val myBlog : string * string * string
> printBlogInfo myBlog;;
Chris's blog [Completely Unique View] is online at 'http://blogs.msdn.com/chrsmith'
val it : unit = ()
5. 函数柯里化(Function Currying)
F#提供的一个新奇的特性是可以只接受参数的一个子集,而接受部分参数的结果则是一个新的函数。这就是所谓的“函数柯里化”。比如,假设有一个函数接受3个整数,返回它们的和。我们可以只传入第一个参数,假设值为10,这样我们就可以说将原来的函数柯里化了,而它会返回一个新的函数——新函数接受两个整数,返回它们与10的和。
> let addThree x y z = x + y + z;;
val addThree : int -> int -> int -> int
> let addTwo x y = addThree 10 x y;;
val addTwo : int -> int -> int
> addTwo 1 1;;
val it : int = 12
6. Union类型(Union Types,Discriminated Unions)
考虑下面的枚举值:
enum CardSuit { Spade = 1, Club = 2, Heart = 3, Diamond = 4};
理论上,一个card实例只有一种可能的取值,但由于enum本质上只是整数,您不能确定它的值是否是有效的,在C#中,你可以这么写:
CardSuit invalid1 = (CardSuit) 9000;
CardSuit invalid2 = CardSuit.Club | CardSuit.Diamond;
另外,考虑下面的情形。如果您需要扩展一个enum:
enum Title { Mr, Mrs }
Title枚举可以工作地很好,但一段时间后,如果需要添加一个“Ms”值,那么每一个switch语句都面临一个潜在的bug。当然您可以尝试修复所有的代码,却难免会发生遗漏。
枚举可以很好地表达某些概念,但是却无法提供足够的编译器检查。F#中的Union类型可设定为一组有限的值:数据标签(data tag)。例如,考虑一个表示微软员工的Union:
type MicrosoftEmployee =
| BillGates
| SteveBalmer
| Worker of string
| Lead of string * MicrosoftEmployee list
如果有一个MicrosoftEmployee类型的实例,您就知道它必定是{BillGates,SteveBalmer,Worker,Lead}之一。另外,如果它是Worker,您可以知道有一个字符串与之关联,也许是他的名字。我们可以轻松地创建Union类型,而后使用模式匹配(下一小节)来匹配它们的值。
let myBoss = Lead("Yasir", [Worker("Chris"); Worker("Matteo"); Worker("Santosh")])
let printGreeting (emp : MicrosoftEmployee) =
match emp with
| BillGates -> printfn "Hello, Bill"
| SteveBalmer -> printfn "Hello, Steve"
| Worker(name) | Lead(name, _)
-> printfn "Hello, %s" name
现在假设需要扩展Union类型:
type MicrosoftEmployee =
| BillGates
| SteveBalmer
| Worker of string
| Lead of string * MicrosoftEmployee list
| ChrisSmith
我们会看到一些编译器警告信息:
编译器检测到您没有匹配Union的每一个数据标签,发出了警告。像这样的检查会避免很多bug,要了解
更多的关于Union类型的信息,看这篇文章。
7. 模式匹配(Pattern Matching)
模式匹配看起来像是增强版的switch语句,允许您完成分支型控制流程。除了跟常数值进行比较外,还可以捕获新的值。比如在前面的例子中,我们在匹配Union数据标签时绑定了标识符“name”。
let printGreeting (emp : MicrosoftEmployee) =
match emp with
| BillGates -> printfn "Hello, Bill"
| SteveBalmer -> printfn "Hello, Steve"
| Worker(name) | Lead(name, _)
-> printfn "Hello, %s" name
还可以对数据的“结构”进行匹配,比如对列表(list)进行匹配。(还记得吗,x :: y表示x为列表的一个元素,y是x之后的元素,而[]则是空列表。)
let listLength aList =
match aList with
| [] -> 0
| a :: [] -> 1
| a :: b :: [] -> 2
| a :: b :: c :: [] -> 3
| _ -> failwith "List is too big!"
在这个匹配的最后,我们使用了通配符“_”(下划线),它匹配任意值。如果aList变量包含多于三个的元素,最后的模式子句将执行,并抛出一个异常。模式匹配还可以我们执行任意表达式来确定模式是否匹配(如果表达式的值为false,则不匹配)。
let isOdd x =
match x with
| _ when x % 2 = 0 -> false
| _ when x % 2 = 1 -> true
我们甚至可以使用动态类型测试进行匹配:
let getType (x : obj) =
match x with
| :? string -> "x is a string"
| :? int -> "x is a int"
| :? System.Exception -> "x is an exception"
| :? _ -> "invalid type"
8. 记录类型(Records)
在声明包含若干个公有属性的类型时,记录类型是一种轻量级的方式。它的一个优势是,借助于类型推演系统,编译器可以通过值的声明得出适当的记录类型。
type Address = {Name : string; Address : string; Zip : int}
let whiteHouse = {Name = "The White House"; Address = "1600 Pennsylvania Avenue";
Zip = 20500}
在上面的例子中,首先定义了“Address”类型,那么在声明它的实例时,无须显式地使用类型注解,编译器可根据字段(属性)的名称自行得出类型的信息。所以whiteHouse的类型为Address。
9. Forward Pipe Operator(|>)
|>操作符只是简单地定义为:
let (|>) x f = f x
其类型前面信息为:
'a -> ('a -> 'b) -> 'b
可以这么来理解:x的类型为'a,函数f接受'a类型的参数,返回类型为'b,操作符的结果就是将x传递给f后所求得的值。
还是来看个例子吧:
// Take a number, square it, then convert it to a string, then reverse that string
let square x = x * x
let toStr (x : int) = x.ToString()
let rev (x : string) = new String(Array.rev (x.ToCharArray()))
// 32 -> 1024 -> "1024" -> "4201"
let result = rev (toStr (square 32))
上面的代码是很直白的,但语法看起来却不太好。我们所做的就是将一个运算的结果传给下一个运算。我们可以通过引入几个变量来改写代码为:
let step1 = square 32
let step2 = toStr step1
let step3 = rev step2
let result = step3
但是我们需要维护这几个临时变量。|>操作符接受一个值,将其“转交”给一个函数。这会大大地简化F#代码:
let result = 32 |> square |> toStr |> rev
10. 序列(Sequence,System.Collections.Generic.IEnumerator<_>)
序列(在F#中为seq)是 System.Collections.Generic.IEnumerator的别名,但它在F#中有另外的作用。不像列表和数组,序列可包含无穷个值。只有当前的值保存在内存中,一旦序列计算了下个值,当前的值就会被忘记(丢弃)。例如,下面的代码生成了一个包含所有整数的序列。
let allIntegers = Seq.init_infinite (fun i -> i)
11. 集合(Collections:Seq,List,Array)
在F#中,如果您想表示一个值的集合,至少有三个好的选择——数组、列表和序列,它们都有各自的优点。而且每种类型都有一系列的模块内置于F#库中。您可以使用VS的智能感知来探究这些方法,这里我们来看看最常用的那些:
iter。“iter”函数遍历集合的每一项。这与“foreach”循环是一致的。下面的代码打印列表的每一项:
List.iter (fun i -> printfn "Has element %d" i) [1 .. 10]
map。像我在前面所说的,map函数基于一个指定的函数对集合的值进行转换。下面的例子将数组的整数值转换为它们的字符串表示:
Array.map (fun (i : int) -> i.ToString()) [| 1 .. 10 |]
fold。“fold”函数接受一个集合,并将集合的值折叠为单个的值。像iter和map一样,它接受一个函数,将其应用于集合的每个元素,但它还接受另一个“accumulator”参数。fold函数基于上一次运算不断地累积accumulator参数的值。看下面的例子:
Seq.fold (fun acc i -> i + acc) 10 { 1 .. 10 }
该代码的功能是:以10为基数(acculator),累加序列中的每一项。
只有序列有fold方法,列表和数组则有fold_left和fold_right方法。它们的不同之处在于计算顺序的不同。
12. 可选值(Option Values)
基于函数式编程的特点,在F#中很难见到null值。但有些情况下,null值比未初始化变量更有意义。有时可选值则表示值未提供(可选值就像C#中的nullable类型)。
F#中的“可选类型(option type)”有两种状态:“Some”和“None”。在下面的记录类型Person中,中间的字段可能有值,也可能没有值。
type Person = { First : string; MI : string option; Last : string }
let billg = {First = "Bill"; MI = Some("H"); Last = "Gates" }
let chrsmith = {First = "Chris"; MI = None; Last = "Smith" }
13. 延迟求值(惰性值,Lazy Values,Microsoft.FSharp.Core.Lazy<_>)
延迟初始化表示一些值,它们在需要时才进行计算。F#拥有延迟求值特性。看下面的例子,“x”是一个整数,当对其进行求值时会打印“Computed”。
> let x = lazy (printfn "Computed."; 42);;
val x : Lazy<int>
> let listOfX = [x; x; x];;
val listOfX : Lazy<int> list
> x.Force();;
Computed.
val it : int = 42
可以看到,我们在调用“Force”方法时,对x进行求值,返回的值是42。您可以使用延迟初始化来避免不必要的计算。另外在构造递归值时,也很有用。例如,考虑一个Union值,它用来表示循环列表:
type InfiniteList =
| ListNode of int * InfiniteList
let rec circularList = ListNode(1, circularList)
“circularList”拥有对自身的引用(表示一个无限循环)。不使用延迟初始化的话,声明这样类型的值是不可能的。
现在,您应该对F#的基础有了足够的了解了,下一步,在系列文章的第三部分中,我们将学习一些高级主题——一些F#能做而其他的.NET语言不能做的事情,敬请期待!
接下来我们看看开发F#时可以选用的不同方式。
(一)Notepad
F#程序本质上就是文本文件,所以我们可以使用文本编辑器来编写,比如记事本。它的文件扩展名为.fs,编写完毕后使用fsc.exe来编译。比如,编写一个最简单的文件helloworld.fs:
#light
print_endline "Hello World"
read_line()
使用命令fsc.exe helloworld.fs编译该文件,生成helloworld.exe文件,它将在控制台输出一段文本。注意要将fsc.exe的路径添加到环境变量中。
如果采用文本编辑器的方式,我们当然不会真的使用Notepad,大可以采用Editplus或Notepad++这样的工具,它们不但提供了更强大的编辑功能,还可以添加用户自定义工具,这样就不用每次都打开命令行编译了。
(二)FSI
F#交互控制台(F# Interactive Console, FSI)采用的是“REPL loop”模式,即Read-Evaluate-Print-Loop。也就是输入一段代码,编译并执行,然后输出结果。通过它您可以快速地开发和测试程序。可在开始菜单中找到它。
(三)VFSI(在VS中集成FSI)
要在VS中启用FSI,打开Add-in Manager窗口。
选中“F# Inactive for Visual Studio”。然后,选中要执行的程序代码:
接着按下Alt+Enter(实际上,如果FSI还么有打开,需要按两次Alt+Enter)。这时会看到出现了一个工具窗口:
我们刚才做的事情是将代码片段直接发送给FSI会话,FSI将结果输出。
这是最简单的方式了,我们可以利用VS提供的智能感知,可以查看各个标识符的类型等信息,非常方便。
另外我们可以将fsc.exe添加到外部工具中,为该命令分配快捷键。比如可以这样添加:
然后分配快捷键:
有时FSI会莫名地不能编译通过,通过这样的方式可以方便的将单个文件编译为可执行文件,编译也不会有问题了。
(四)F# Script
除了.fs,我们还可以将代码文件保存为.fsx,在资源管理器中右击该文件,会发现“Run with F# Interactive”菜单项,选择它就可以执行代码了,这种方式适合于小型文件,可以随时修改,无须编译。
(五)SharpDevelop
最新版的SharpDevelop提供了对F#项目的支持,但是不支持智能感知。我觉得最有用的是它的文件顺序调整功能,如果你的项目包含较多的文件,可以试试它。
最后,建议主要使用第三种方式即VFSI开发,其它的作为补充。
函数式编程范式简介
F#主要支持三种编程范式:函数式编程(Functional Programming,FP)、命令式编程(Imperative Programming)和面向对象(Object-Oriented,OO)的编程。回顾它们的历史,FP是最早的一种范式,第一种FP语言是IPL,产生于1955年,大约在Fortran一年之前。第二种FP语言是Lisp,产生于1958,早于Cobol一年。Fortan和Cobol都是命令式编程语言,它们在科学和商业领域的迅速成功使得命令式编程在30多年的时间里独领风骚。而产生于1970年代的面向对象编程则不断成熟,至今已是最流行的编程范式。有道是“江山代有语言出,各领风骚数十年”。
尽管强大的FP语言(SML,Ocaml,Haskell及Clean等)和类FP语言(APL和Lisp是现实世界中最成功的两个)在1950年代就不断发展,FP仍停留在学院派的“象牙塔”里;而命令式编程和面向对象编程则分别凭着在商业领域和企业级应用的需要占据领先。今天,FP的潜力终被认识——它是用来解决更复杂的问题的(当然更简单的问题也不在话下)。
纯粹的FP将程序看作是接受参数并返回值的函数的集合,它不允许有副作用(side effect,即改变了状态),使用递归而不是循环进行迭代。FP中的函数很像数学中的函数,它们都不改变程序的状态。举个简单的例子,一旦将一个值赋给一个标识符,它就不会改变了,函数不改变参数的值,返回值是全新的值。
FP的数学基础使得它很是优雅,FP的程序看起来往往简洁、漂亮。但它无状态和递归的天性使得它在处理很多通用的编程任务时没有其它的编程范式来得方便。但对F#来说这不是问题,它的优势之一就是融合了多种编程范式,允许开发人员按照需要采用最好的范式。
关于FP的更多内容建议阅读一下这篇文章:Why Functional Programming Matters(中文版)。
F#中的函数式编程
从现在开始,我将对F#中FP相关的主要语言结构逐一进行介绍。
标识符(Identifier)
在F#中,我们通过标识符给值(value)取名字,这样就可以在后面的程序中引用它。通过关键字let定义标识符,如:
let x = 42
这看起来像命令式编程语言中的赋值语句,两者有着关键的不同。在纯粹的FP中,一旦值赋给了标识符就不能改变了,这也是把它称为标识符而非变量(variable)的原因。另外,在某些条件下,我们可以重定义标识符;在F#的命令式编程范式下,在某些条件下标识符的值是可以修改的。
标识符也可用于引用函数,在F#中函数本质上也是值。也就是说,F#中没有真正的函数名和参数名的概念,它们都是标识符。定义函数的方式与定义值是类似的,只是会有额外的标识符表示参数:
let add x y = x + y
这里共有三个标识符,add表示函数名,x和y表示它的参数。
关键字和保留字
关键字是指语言中一些标记,它们被编译器保留作特殊之用。在F#中,不能用作标识符或类型的名称(后面会讨论“定义类型”)。它们是:
abstract and as asr assert begin class default delegate do done
downcast downto elif else end exception extern false finally for
fun function if in inherit inline interface internal land lazy let
lor lsr lxor match member mod module mutable namespace new null
of open or override private public rec return sig static struct
then to true try type upcast use val void when while with yield
保留字是指当前还不是关键字,但被F#保留做将来之用。可以用它们来定义标识符或类型名称,但编译器会报告一个警告。如果你在意程序与未来版本编译器的兼容性,最好不要使用。它们是:
atomic break checked component const constraint constructor continue
eager event external fixed functor global include method mixin
object parallel process protected pure sealed trait virtual volatile
文字值(Literals)
文字值表示常数值,在构建计算代码块时很有用,F#提供了丰富的文字值集。与C#类似,这些文字值包括了常见的字符串、字符、布尔值、整型数、浮点数等,在此不再赘述,详细信息请查看F#手册。
与C#一样,F#中的字符串常量表示也有两种方式。一是常规字符串(regular string),其中可包含转义字符;二是逐字字符串(verbatim string),其中的(")被看作是常规的字符,而两个双引号作为双引号的转义表示。下面这个简单的例子演示了常见的文字常量表示:
let message = "Hello World"r"n!" // 常规字符串
let dir = @"C:"FS"FP" // 逐字字符串
let bytes = "bytes"B // byte 数组
let xA = 0xFFy // sbyte, 16进制表示
let xB = 0o777un // unsigned native-sized integer,8进制表示
let print x = printfn "%A" x
let main() =
print message;
print dir;
print bytes;
print xA;
print xB;
main()
Printf函数通过F#的反射机制和.NET的ToString方法来解析“%A”模式,适用于任何类型的值,也可以通过F#中的print_any和print_to_string函数来完成类似的功能。
值和函数(Values and Functions)
在F#中函数也是值,F#处理它们的语法也是类似的。
let n = 10
let add a b = a + b
let addFour = add 4
let result = addFour n
printfn "result = %i" result
可以看到定义值n和函数add的语法很类似,只不过add还有两个参数。对于add来说a + b的值自动作为其返回值,也就是说在F#中我们不需要显式地为函数定义返回值。对于函数addFour来说,它定义在add的基础上,它只向add传递了一个参数,这样对于不同的参数addFour将返回不同的值。考虑数学中的函数概念,F(x, y) = x + y,G(y) = F(4, y),实际上G(y) = 4 + y,G也是一个函数,它接收一个参数,这个地方是不是很类似?这种只向函数传递部分参数的特性称为函数的柯里化(curried function)。
当然对某些函数来说,传递部分参数是无意义的,此时需要强制提供所有参数,可是将参数括起来,将它们转换为元组(tuple)。下面的例子将不能编译通过:
let sub(a, b) = a - b
let subFour = sub 4
必须为sub提供两个参数,如sub(4, 5),这样就很像C#中的方法调用了。
对于这两种方式来说,前者具有更高的灵活性,一般可优先考虑。
如果函数的计算过程中需要定义一些中间值,我们应当将这些行进行缩进:
let halfWay a b =
let dif = b - a
let mid = dif / 2
mid + a
需要注意的是,缩进时要用空格而不是Tab,如果你不想每次都按几次空格键,可以在VS中设置,将Tab字符自动转换为空格;虽然缩进的字符数没有限制,但一般建议用4个空格。而且此时一定要用在文件开头添加#light指令。
作用域(Scope)
作用域是编程语言中的一个重要的概念,它表示在何处可以访问(使用)一个标识符或类型。所有标识符,不管是函数还是值,其作用域都从其声明处开始,结束自其所处的代码块。对于一个处于最顶层的标识符而言,一旦为其赋值,它的值就不能修改或重定义了。标识符在定义之后才能使用,这意味着在定义过程中不能使用自身的值。
let defineMessage() =
let message = "Help me"
print_endline message // error
对于在函数内部定义的标识符,一般而言,它们的作用域会到函数的结束处。
但可使用let关键字重定义它们,有时这会很有用,对于某些函数来说,计算过程涉及多个中间值,因为值是不可修改的,所以我们就需要定义多个标识符,这就要求我们去维护这些标识符的名称,其实是没必要的,这时可以使用重定义标识符。但这并不同于可以修改标识符的值。你甚至可以修改标识符的类型,但F#仍能确保类型安全。所谓类型安全,其基本意义是F#会避免对值的错误操作,比如我们不能像对待字符串那样对待整数。这个跟C#也是类似的。
let changeType() =
let x = 1
let x = "change me"
let x = x + 1
print_string x
在本例的函数中,第一行和第二行都没问题,第三行就有问题了,在重定义x的时候,赋给它的值是x + 1,而x是字符串,与1相加在F#中是非法的。
另外,如果在嵌套函数中重定义标识符就更有趣了。
let printMessages() =
let message = "fun value"
printfn "%s" message;
let innerFun () =
let message = "inner fun value"
printfn "%s" message
innerFun ()
printfn "%s" message
printMessages()
打印结果:
fun value
inner fun value
fun value
最后一次不是inner fun value,因为在innerFun仅仅将值重新绑定而不是赋值,其有效范围仅仅在innerFun内部。
递归(Recursion)
递归是编程中的一个极为重要的概念,它表示函数通过自身进行定义,亦即在定义处调用自身。在FP中常用于表达命令式编程的循环。很多人认为使用递归表示的算法要比循环更易理解。
使用rec关键字进行递归函数的定义。看下面的计算阶乘的函数:
let rec factorial x =
match x with
| x when x < 0 -> failwith "value must be greater than or equal to 0"
| 0 -> 1
| x -> x * factorial(x - 1)
这里使用了模式匹配(F#的一个很棒的特性),其C#版本为:
public static long Factorial(int n)
{
if (n < 0) { throw new ArgumentOutOfRangeException("value must be greater than or equal to 0"); }
if (n == 0) { return 1; }
return n * Factorial (n - 1);
}
递归在解决阶乘、Fibonacci数列这样的问题时尤为适合。但使用的时候要当心,可能会写出不能终止的递归。
匿名函数(Anonymous Function)
定义函数的时候F#提供了第二种方式:使用关键字fun。有时我们没必要给函数起名,这种函数就是所谓的匿名函数,有时称为lambda函数,这也是C#3.0的一个新特性。比如有的函数仅仅作为一个参数传给另一个函数,通常就不需要起名。在后面的“列表”一节中你会看到这样的例子。除了fun,我们还可以使用function关键字定义匿名函数,它们的区别在于后者可以使用模式匹配(本文后面将做介绍)特性。看下面的例子:
let x = (fun x y -> x + y) 1 2
let x1 = (function x -> function y -> x + y) 1 2
let x2 = (function (x, y) -> x + y) (1, 2)
我们可优先考虑fun,因为它更为紧凑,在F#类库中你能看到很多这样的例子。
注意:本文中的代码均在F# 1.9.4.17版本下编写,在F# CTP 1.9.6.0版本下可能不能通过编译。
操作符(Operator)
F#中,可把操作符看作一种函数调用的更为优雅的方式。操作符有两种:前缀(prefix)和中缀(infix),前者接受一个操作数(operand),出现在操作数之前;后者接受两个或多个操作数,出现在头两个操作数之间。
F#提供了丰富的操作符集,可用于数字、布尔值、字符串和集合类型。这些操作符数量甚众,限于篇幅,在此不再一一详解。本文将着重介绍如何使用和定义操作符。
类似于C#,F#的操作符也可以重载,也就是说,我们可以将不同的类型用于同一操作符,如“+”;但是与C#不同的是,各个操作数必须为相同的类型。F#的操作符重载规则与C#类似,因此任何BCL或者使用C#编写的.NET类库中的支持操作符重载的类在F#中一样支持重载。
let words = "To live " + "is " + " to function."
open System
let oneYearLater = DateTime.Now + new TimeSpan(365, 0, 0, 0, 0)
我们可以定义自己的操作符,也可以重定义已有的任何操作符(不建议这样做)。看看下面这种不好的做法:
let (+) a b = a – b
print_int (1 + 2)
看到这里,你想到了什么?是不是:这分明是在定义一个函数嘛!所以我们前面说“可把操作符看作一种函数调用的更为优雅的方式”。我们重定义了“+”操作符,所以“1 + 2”的结果为-1,这当然没什么好处,在VS中使用FSI时,怎样把“+”改回它原本的含义呢?我一般在任务管理器中把fsi进程关掉,再按回车,“+”就回来了。
自定义的操作符不能包含字母和数字,可以使用的字符如下:
!$%&+-./<=>?@^|~
:
操作符的第一个字符可以是上面第一行的任意字符,其后的字符则可以是上面的任意字符了。定义语法与函数类似,除了要将操作括起来。看下面的例子:
let (+^*) a b = (a + b) * (a * b)
结果为30。
列表(Lists)
列表是内置于F#的简单集合类型。可以是一个空表(empty list),使用方括号表示([])。我们可以将一个值与列表连接,此时要使用“::”操作符,注意要将值作为第一个操作数:
let emptyList = []
let oneItem = "one" :: []
let twoItem = "two" :: oneItem
在VS中可以看到oneItem的类型为string list。如果列表包含多个项,用上面的方法显得麻烦了,我们可以使用下面的语法:
let shortHand = ["hello "; "world!"]
另外我们还可使用“@”操作符来连接两个列表:
let concatenateLists = ["one, "; "two, "] @ ["three, "; "four"]
F#要求列表中的元素类型必须是相同的,如果你确实需要列表包含不同类型的元素,那只好创建一个obj(即System.Object)类型的列表了:
let objList = [box 1; box 2.0; box "three"]
其中第三个box是可选的,这个让我想起了C#中的装箱。
F#中的列表是不可修改的,一旦创建就不能修改了。作用于列表的函数和操作符也不能修改列表,而是创建了列表的一个副本。这个特性很像C#中的string类型。看下面的例子:
#light
let printList list =
List.iter print_string list
print_newline()
let threeItems = ["one "; "two "; "three "]
let reversedList = List.rev threeItems
printList threeItems
printList reversedList
上面的iter方法接受两个参数,第一个是函数,第二个是列表,其作用是将函数依次应用于列表的每个元素,有点像C#中的foreach循环。而rev方法则返回列表的逆序列表。
打印结果为:
one tow three
three two one
上述两个方法都没有改变原来的列表。要了解关于F#列表的更多信息,建议阅读这篇文章:Mastering F# Lists。
列表推导(List Comprehensions)
列表推导的概念源于数学,它使得创建和转换集合的操作变得简单。在F#中可以使用这样的推导语法直接创建列表,序列(sequence)和数组(序列和数组将在后面介绍)。要了解这个概念的更多内容可以查看:List_comprehension。
最简单的情况是指定列表的范围,如:
let numericList = [0 .. 9]
let charList = ['A' .. 'Z']
这两个列表的类型分别是int list和char list,范围分别是从0到9和从’A’到’Z’。
更复杂的情况是指定一个步长:
let multipleOfThree = [0 .. 3 .. 30]
let revNumericList = [9.. -1 .. 0]
第一个列表的值是0到30间所有3的倍数,第二个列表的元素则包含了从9递减至0。
我们还可以通过对一个列表进行循环操作得到另一个列表。例如:
let squares = [for x in 1 .. 10 -> x * x]
通过for进行循环,squares列表的元素是1到10间的整数的平方。
此外还可以为循环添加when子句对元素进行过滤,只有when子句的值为true时才对其进行运算:
let evens = [for x in 1 .. 10 when x % 2 = 0 -> x]
evens的元素为[2; 4; 6; 8; 10]。
控制流程(Control Flow)
F#拥有强的控制流程概念,这与很多纯函数式编程语言不同,在这些语言中表达式可以以任何顺序进行求值。看下面的例子:
let absoluteValue x =
if x < 0 then
-x
elif x = 0 then
0
else
x
if, elif, then, else组成的结构我们应当很熟悉,在F#中该结构是一个表达式,也就是说它需要返回一个值。而且每个分支返回的值应当具有相同的类型,否则就会有编译错误。如果确实要返回多个类型的值,在值前加box关键字,就像前面创建列表时那样,这样表达式的返回类型为obj。
类型与类型推导(Types and Type Inference)
F#是一种强类型的语言,传给函数的值必须是指定的类型。如果函数接受string类型的参数,就不能传给它int类型的值。一种语言处理其中值的类型的方式称为语言的类型系统。F#的类型系统与一般语言不同,包括函数在内,所有的值都具有自己的类型。
通常情况下,我们不需要显式地声明类型,编译器会尝试从值的文字值或调用的函数返回类型来判断其类型,这个过程称为类型推导。可在编译时使用-i开关来显示所有的推导类型,在VS中我们则可以使用工具提示来查看标识符的类型。先看下面值的类型推导情况:
let strValue = "String Value"
let intValue = 12
在fsi中可看到它们的信息是:
val strValue : string
val intValue : int
可以理解,编译器跟据赋给标识符的文字值来推导其类型。再看看下面函数的情况:
let makeMessage x = (string_of_bool x) + " is a boolean value"
let half x = x / 2
在fsi中可看到它们的信息是:
val makeMessage : bool -> string
val half : int -> int
有意思的是,函数名前面也有个val,这表明函数也是值,后面的bool -> string是什么意思呢?它表明函数接受bool类型参数,返回string类型的值,注意x作为string_of_bool的参数,所以x必须为bool类型,返回值是两个字符串相加的值,故返回值也是string类型。对于half函数,单从定义不能确定x类型,此时编译器采用默认的类型int。再看看稍微复杂点的情况:
let div1 x y = x / y
let div2 (x, y) = x / y
这两个函数的信息是:
val div1 : int -> int -> int
val div2 : int * int -> int
div1函数可接受部分参数(可柯里化),而div2则必须同时传入两个int类型的值。考虑下面的函数:
let doNothing x = x
其信息为:
val doNothing : 'a -> 'a
a’ -> a’表示函数接受任意类型,并返回与其相同类型的值。以’打头的类型表示可变类型(variable type),编译器虽然不能确定类型的参数,却能确定返回值类型必须与参数类型相同,类型系统的这种特性称为类型参数化,通过它编译器也能发现更多的类型错误。可变类型或类型参数化的概念,类似于.NET 2.0的泛型,如果F#基于支持泛型的CLI,那么它会充分利用泛型的优势。另外,F#的创建者Don Syme,正是CLR中泛型的设计者和实现者。
F#的类型推导固然强大,但它显然不能揣测出开发人员所有的心思来,如果有特殊需求该怎么办呢?看下面的例子:
let doNothingToFloat (x : float32) = x
float32即System.Single,这里我们手动指定了x的类型,这个有时称为类型标注(type annotation)。如果要在F#中使用其它.NET语言编写的类库,或者与非托管的类库进行互操作,它会很有用。
小结
本文继续介绍F#中的函数式编程范式,主要包含了操作符、列表、列表推导、类型推导、类型标注等概念。类型推导又称隐式类型,通常是——但不限于——函数式编程语言的特性,比如C# 3.0和VB.NET 9.0都提供了一定的支持,它使很多编程任务变得更为简单。
参考:
《Foundations of F#》 by Robert Pickering
《F# Specs》
模式匹配(Pattern Matching)
模式匹配允许你根据标识符值的不同进行不同的运算。有点像一连串的if...else结构,也像C++和C#中的switch,但是它更为强大和灵活。
看下面Lucas序列的例子,Lucas序列定义跟Fibonacci序列一样,只不过起始值不同:
let rec luc x =
match x with
| x when x <= 0 -> failwith "value must be greater than zero"
| 1 -> 1
| 2 -> 3
| x -> luc(x - 1) + luc(x - 2)
printfn "(luc 2) = %i" (luc 2)
printfn "(luc 6) = %i" (luc 6)
这里可以看到模式匹配的简单应用,使用关键字match和with,不同的模式规则间用“|”隔开,而“->”表示如果该模式匹配,运算结果是什么。
这个例子的打印结果为:
Output
(luc 2) = 3
(luc 6) = 18
匹配规则时按照它们定义的顺序,而且模式匹配必须完整定义,也就是说,对于任意一个可能的输入值,都有至少一个模式能够满足它(即能处理它);否则编译器会报告一个错误信息。另外,排在前面的规则不应比后面的更为“一般”,否则后面的规则永远不会得到匹配,编译器会报告一个警告信息,这种方式很像C#中的异常处理方式,在捕获异常时,我们不能先不会“一般”的Exception异常,然后再捕获“更具体”的NullReferenceException异常。
可以为某个模式规则添加一个when卫语句(guard),可将when语句理解为对当前规则的更强的约束,只有when语句的值为true时,该模式规则才算匹配。在上例的第一个规则中,如果没有when语句,那么任意整数都能够匹配模式,加了when语句后,就只能匹配非正整数了。对于最简单的情况,我们可以省略第一个“|”:
let boolToString x =
match x with false -> "False" | _ -> "True"
这个例子中包含两个模式规则,“_”可以匹配任意值,因此当x值为false时匹配第一个规则,否则就匹配第二个规则。
另一个有用的特性是,我们可以合并两个模式规则,对它们采取相同的处理方式,这个就像C#中的switch…case结构中可以合并两个case一样。
let stringToBool x =
match x with
| "T" | "True" | "true" -> true
| "F" | "False" | "false" -> false
| _ -> failwith "Invalid input."
在本例中,我们把三种模式规则合并在了一起,将字符串值转换为相应的布尔值。
可以对大多数F#中定义的类型进行模式匹配,下面的例子就展示了对元组进行匹配。
let myOr b1 b2 =
match b1, b2 with
| true, _ -> true
| _, true -> true
| _ -> false
let myAnd p =
match p with
| true, true -> true
| _ -> false
这两个函数说明了如何对元组应用模式匹配,它们的功能是求两个布尔值“或”和“且”运算的结果。在myOr中,从第一、二两个模式规则可以知道b1、b2只要有一个为true,计算结果就是true,否则为false。myOr true false的结果为true,myAnd(true, false)结果为false。
模式匹配的常见用法是对列表进行匹配,事实上,对列表来说,较之if…then…else结构,模式匹配的方式更好。看下面的例子:
let listOfList = [[2; 3; 5]; [7; 11; 13]; [17; 19; 23; 29]]
let rec concatenateList list =
match list with
| head :: tail -> head @ (concatenateList tail)
| [] -> []
let rec concatenateList2 list =
if List.nonempty list then
let head = List.hd list in
let tail = List.tl list in
head @ (concatenateList2 tail)
else
[]
let primes = concatenateList listOfList
print_any primes
listOfList是一个列表的列表,两个函数concatenateList和concatenateList2的功能都是将listOfList的元素连接为一个大的列表,只不过一个用模式匹配方式实现,一个使用if…then…else结构实现。可以看到使用模式匹配的代码更为简洁明了。观察concatenateList函数,它处理列表的方式是先取出列表的头元素(head),处理它,然后递归地处理剩余元素,这其实是通过模式匹配方式处理列表的最常见的方式(但不是唯一的方式)。
在F#中,模式匹配还可用在其它地方,在后面的文章中将陆续介绍。
定义类型(Defining Types)
F#的类型系统提供了若干特性,可用来创建自定义类型。所有的类型可分为两类,一是元组(Tuple)或记录(Record),它们类似于C#中的类;二是Union类型,有时称为Sum类型。下面分别来看一下它们的特点。
元组是任意对象的有序集合,通过它我们可以快速、方便地将一组值组合在一起。创建之后,就可以引用元组中的值。
let pair = true, false
let b1, b2 = pair
let _, b3 = pair
let b4, _ = pair
第一行代码创建了一个元组,其类型为bool * bool, 说明pair元组包含两个值,它们的类型都是bool。通过第二、三、四行这样的代码,可以访问元组的值,“_”告诉编译器,我们对该值不感兴趣,将其忽略。这里b1的值为true,b3的值为false。
进一步分析,元组是一种类型,但是我们并没有显式地使用type关键字来声明类型,pair本质上是F#中Tuple类的一个实例,而不是自定义类型。如果需要声明自定义类型,就要使用type关键字了,最简单的情况是给已有类型起个别名:
type Name = string
// FirstName, LastName
type FullName = string * string
对于Name类型来说,它仅仅是string类型的别名,FullName则是元组类型的别名。
记录(Record)类型与元组有相似之处,它也是将多个类型的值组合为同一类型,不同之处在于记录类型中的字段都是有名称的。看下面的例子:
type Organization = { Boss : string; Lackeys : string list }
let family =
{ Boss = "Children";
Lackeys = ["Wife"; "Husband"] }
第一行是创建Organization类型,第二行则是创建它的实例,令人惊奇的是不需要声明实例的类型,F#编译器能够根据字段名推导出它的类型。这个功能是很强大,但是F#不强求每个类型的字段都是不同的,如果两个类型的各个字段名都一样怎么办呢?这时可以显式地声明类型:
type Company = { Boss : string; Lackeys : string list }
let myCom =
{ new Company
with Boss = "Bill"
and Lackeys = ["Emp1"; "Emp2"] }
一般情况下,类型的作用域从声明处至所在源文件的结束。如果一个类型要使用在它后面声明的类型,可将这两个类型声明在同一个代码块中。类型间用and分隔,看下面食谱的例子:
type recipe =
{ recipeName : string;
ingredients : ingredient list;
instructions : string }
and ingredient =
{ ingredientName : string;
quantity : int }
let greenBeansPineNuts =
{ recipeName = "Green Beans & Pine Nuts";
ingredients =
[{ingredientName = "Green beans"; quantity = 200};
{ingredientName = "Pine nuts"; quantity = 200}];
instructions = "Parboil the green beans for about 7 minutes." }
let name = greenBeansPineNuts.recipeName
let toBuy =
List.fold_left
(fun acc x ->
acc + (Printf.sprintf "\t%s - %i\r\n" x.ingredientName x.quantity))
"" greenBeansPineNuts.ingredients
let instructions = greenBeansPineNuts.instructions
printf "%s\r\n%s\r\n\r\n\t%s" name toBuy instructions
本例不仅展示了如何将两个类型声明在“一块”,还显示了如何访问记录的字段值。可以看到访问记录的字段要比访问元组的值更为方便。
也可以对记录类型应用模式匹配:
type couple = { him : string; her : string }
let couples =
[ { him = "Brad"; her = "Angelina" };
{ him = "Becks"; her = "Posh" };
{ him = "Chris"; her = "Gwyneth" } ]
let rec findDavid list =
match list with
| { him = x; her = "Posh" } :: tail -> x
| _ :: tail -> findDavid tail
| [] -> failwith "Couldn't find David"
print_string(findDavid couples)
首先创建了couple类型的列表,findDavid函数将对该列表进行模式匹配,可以将字段与常量值比较,如her = “Posh”;将字段值赋给标识符,如him = x;还可以使用“_”忽略某个字段的值。最后上面例子的打印结果:Becks。
字段值也可以是函数,这种技术将在本系列文章的第三部分介绍。
Union类型,有时称为sum类型或discriminated union,可将一组具有不同含义或结构的数据组合在一起。可与C语言中的联合或C#中的枚举类比。先来看个例子:
type Volume =
| Liter of float
| UsPint of float
| ImperialPint of float
Volume类型属于Union类型,包含3个数据构造器(Data Constructor),每个构造器都包含单一的float值。声明其实例非常简单:
let vol1 = Liter 2.5
let vol2 = UsPint 2.5
let vol3 = ImperialPint 2.5
事实上,通过Reflector可以看到,Liter、UsPint和ImperialPint是Volume类型的派生类。在将Union类型解析为其基本类型时,我们需要模式匹配。
let convertVolumeToLiter x =
match x with
| Liter x -> x
| UsPint x -> x * 0.473
| ImperialPint x -> x * 0.568
记录类型和Union类型都可以被参数化(Parameterized)。参数化的意思是在一个类型的定义中,它使用了一个或多个其它类型,这些类型不是在定义中确定的,而是在该代码的客户代码中确定。这与前面提及的可变类型是类似的概念。
对于类型参数化,F#中有两种语法予以支持。来看第一种:
OCaml-Style
type 'a BinaryTree =
| BinaryNode of 'a BinaryTree * 'a BinaryTree
| BinaryValue of 'a
let tree1 =
BinaryNode(
BinaryNode(BinaryValue 1, BinaryValue 2),
BinaryNode(BinaryValue 3, BinaryValue 4))
在type关键字和类型名称BinaryTree之家添加了’a,而’a就是可变的类型,它的确切类型将在使用它的代码中确定,这是OCaml风格的语法。在标识符tree1中,定义BinaryValue时用的值是1,编译器将’a解析为int类型。再看看第二种语法:
.NET-Style
type Tree<'a> =
| Node of Tree<'a> list
| Value of 'a
let tree2 =
Node( [Node([Value "One"; Value "Two"]);
Node([Value "Three"; Value "Four"])])
这种语法更接近于C#中的泛型定义,是.NET风格的语法。在tree2中,’a被解析为string类型。不管哪种语法,都是单引号后跟着字母,我们一般只使用单个字母。
创建和使用参数化类型的实例跟非参数化类型的过程是一样的,因为编译器会自动推导参数化的类型。
在本节中,我们逐一讨论了元组、记录和Union类型。通过Reflector可以看到,元组值是Tuple类型的实例,而Tuple实现了Microsoft.FSharp.Core.IStructuralHash和System.IComparable接口;记录和Union则直接实现了这两个接口。要了解IStructualHash接口的更多内容,请参考Jome Fisher的文章。
到这里,我们讨论完了如何定义类型、创建和使用它们的实例,却未提及对它们的修改。那是因为我们没法修改这些类型的值,这是函数式编程的特性之一。但F#提供了多种编程范式,对某些类型来说,它们是可修改的,这将在下一部分(命令式编程)进行介绍。
异常处理(Exception Handling)
在F#中,异常的定义类似于Union类型的定义,而异常处理的语法则类似于模式匹配。使用exception关键字来定义异常,可选地,如果异常包含了数据我们应当声明数据的类型,注意是可以包含多种类型数据的:
exception SimpleException
exception WrongSecond of int
// Hour, MInute, Second
exception WrongTime of int * int * int
要抛出一个异常,可使用raise关键字。F#还提供了另一种方法,如果仅仅想抛出一个包含文本信息的异常,可以使用failwith函数,该函数抛出一个FailureException类型的异常。
let testTime() =
try
let now = System.DateTime.Now in
if now.Second < 10 then
raise SimpleException
elif now.Second < 30 then
raise (WrongSecond now.Second)
elif now.Second < 50 then
raise (WrongTime (now.Hour, now.Minute, now.Second))
else
failwith "Invalid Second"
with
| SimpleException ->
printf "Simple exception"
| WrongSecond s ->
printf "Wrong second: %i" s
| WrongTime(h, m, s) ->
printf "Wrong time: %i:%i:%i" h m s
| Failure str ->
printf "Error msg: %s" str
testTime()
这个例子展示了如何抛出和捕获各种异常,如果你熟悉C#中的异常处理,对此应该不会感到陌生。
与C#类似,F#也支持finally关键字,它当然要与try关键字一起使用。不管是否有异常抛出,finally块中的代码都会执行,在下面的例子中使用finally块来保证文件得以正确地关闭和释放:
let writeToFile() =
let file = System.IO.File.CreateText("test.txt") in
try
file.WriteLine("Hello F# Fans")
finally
file.Dispose()
writeToFile()
需要注意的是,由于CLR架构的原因,抛出异常的代价是很昂贵的,因此要谨慎使用。
延迟求值(或惰性求值,Lazy Evaluation)
第一次接触Lazy的东西是iBATIS中的LazyLoad,也就是延迟加载,它并不是在开始时加载所有数据,而是在必要时才进行读取。延迟求值与此类似,除了性能的提升外,还可用于创建无限的数据结构。
延迟求值与函数式编程语言关系密切,其原理是如果一种语言没有副作用,编译器或运行时可随意选择表达式的求值顺序。F#允许函数具有副作用,因此编译器或运行时不能够按随意地顺序对函数求值,可以说F#具有严格的求值顺序或 F#是一种严格的语言。
如果要利用延迟求值的特性,必须要显式地声明哪些表达式的求值需要延迟,这个要使用lazy关键字。如果需要对该表达式求值,则要调用Lazy模块的force函数。在调用force函数的时候,它会计算表达式的值,而所求得的值会被缓存起来,再次对表达式应用force函数时,所得的值其实是缓存中的值。
let sixtyWithSideEffect = lazy(printfn "Hello, sixty!"; 30 + 30)
print_endline "Force value the first time:"
let actualValue1 = Lazy.force sixtyWithSideEffect
print_endline "Force value the second time:"
let actualValue2 = Lazy.force sixtyWithSideEffect
打印结果为:
Force value the first time:
Hello, sixty!
Force value the second time:
小节
本文继续讨论F#函数式编程范式的核心内容,主要是模式匹配、自定义类型、异常处理和延迟求值等内容,至此,F#的函数式编程的相关内容就介绍完了。模式匹配可以很大程度上简化我们的程序;自定义类型则可以帮助我们更好地组织程序;延迟求值不仅能够提升性能,还可用于创建无限的数据结构,比如自然数序列。另外,在开发F#程序时,建议常用Reflector来看看编译后代码的样子,来了解它优雅的函数式编程背后到底是什么。在下一站,我们将看看命令式编程的风景。
参考:
《Foundations of F#》 by Robert Pickering
《Expert F#》 by Don Syme , Adam Granicz , Antonio Cisternino
《F# Specs》
我们了解了如何使用F#进行纯粹的函数式编程。但是在一些情况下,比如I/O,几乎不能避免改变状态,也就是说会带来side effect。F#并不强求你以无状态的方式编写程序,它提供了可修改(mutable)的标识符来解决这类问题,同时它还提供了其它的程序结构以支持命令式编程。现在就来对这些特性探个究竟。
首先是unit类型,这种类型表示“没有值”。然后是F#如何处理可修改的值。最后来看看如何在F#中使用.NET类库,包括如何调用静态方法、创建对象并使用其成员、使用类的索引器和事件以及F#中的|>操作符。
unit类型
没有参数和返回值的函数的类型为unit,它类似于C#中的void,或者说CLR中的System.Void。对于使用函数式编程语言的开发人员来说,不接受参数、无返回值的函数没多大意义。而在命令式编程中,由于side effect的存在,这样的函数仍有意义。unit类型表示为一对括号“()”。
let main() =
()
在这个例子中,main函数的类型为unit -> unit,也就是说它既不接受参数,也无返回值。第一对括号使得main成为一个函数而不是一个简单的值。第二对括号告诉编译器,main函数什么值也不返回。
注意:仅仅将函数命名为main不表示它就是程序的入口,它不会自动执行(像C#的Main方法那样),要执行它,需要在源文件的结尾处调用:main(),在后面的文章中将介绍如何指定F#程序的入口。
再来看看如何调用unit类型的函数,有两种方式:
let () = main()
// -- or --
main()
我们还可以在函数内部连续多次调用unit类型的函数——只要保证它们的缩进是一样的即可:
let poem() =
print_endline "I sent thee late a rosy wreath"
print_endline "Not so much honouring thee"
print_endline "As giving it a hope that there"
print_endline "It could not withered be"
poem()
这是《Song to Celia》中的四句诗词,print_endline函数的类型为string -> unit,我们知道函数的返回值为其函数体内最后一步运算的值,所以poem的类型为unit -> unit。
并不是说只有unit类型的函数可以这么用,但是调用非unit类型的函数时编译器会报告一个警告,因为它可能会有副作用(side effect),比如:warning FS0020: This expression should have type 'unit', but has type 'string',我们看到警告总会感觉不舒服。F#提供了一些机制可将这些警告消除,即将函数转换为unit类型的函数。事实上,这种需求在使用F#库时不多,在使用由其它语言编写的.NET类库时会更多些。看下面的例子:
#light
let getString() = "foo bar"
let _ = getString()
// -- or --
ignore(getString())
// -- or --
getString() |> ignore
首先是函数getString的定义,下面的三行则是将其转换为unit类型函数的三种方式。第一种是使用下划线“_”,它在前面已经出现过几次,它一般表示我们对某些值不感兴趣;既然不感兴趣,那就可以忽略(ignore)了,这就是第二种方式:ignore函数;第三种方式使用了“|>”操作符,它本质上同第二种一样。“|>”操作符会在稍后介绍。
mutable关键字
在探险之旅(二)中我们知道可以使用let关键字将值绑定至标识符,在某些情况下,我们还可以重定义(redefine)标识符或者绑定(rebind)新的值,但是不能直接修改它的值。显然,对于我们这些用惯了命令式编程语言的人来说,这实在有些不爽,因为这些语言以修改变量的值作为最基本的运算方式。既然F#也支持命令式编程范式,它当然也能让你修改标识符的值。这就是mutable关键字和“<-”操作符,“<-”的类型为unit(操作符也是函数),下面的例子对此作了演示:
let mutable phrase = "Good good study, "
print_endline phrase
phrase <- "day day up."
print_endline phrase
运行结果为:
Good good study,
day day up.
这看起来像是重定义标识符,其实不然。修改标识符时,只能修改它的值而不能修改类型;重定义时可同时改变类型和值(这本质上是定义了一个新的标识符)。事实上,如果你尝试修改标识符的类型,编译器会给你一个错误。另外,它们还有一个重要的差别,即它们的可见性(或者说修改行为的作用域)。在重定义标识符的时候,修改仅仅在新标识符的作用域内有效,一旦出了这个作用域,它就会回复到旧有的值;对于可修改的标识符来说,任何修改都是永久性,与作用域无关。
let redefineX() =
let x = "One"
printfn "Redefining: \r\nx = %s" x
if true then
let x = "Two"
printfn "x = %s" x
else ()
printfn "x = %s" x
let mutableX() =
let mutable x = "One"
printfn "Mutating: \r\nx = %s" x
if true then
x <- "Two"
printfn "x = %s" x
else ()
printfn "x = %s" x
redefineX()
mutableX()
运行结果为:
Redefining:
X = One
X = Two
X = One
Mutating:
X = One
X = Two
X = Two
可修改的标识符也有其局限性,在子函数内不能修改它的值。而这也是ref类型的来由,稍后你会看到。
定义可修改的记录(Record)类型
默认情况下,记录类型是不可变的。不过F#提供了一种特殊的语法,使得我们可以修改记录类型的字段值,即在字段前使用mutable关键字。需要注意的是,这种操作改变的是记录字段的内容而不是记录本身。
type Couple =
{her : string; mutable him : string}
let couple = {her = "Elizabeth Taylor"; him = "Nicky Hilton"}
let print o = printf "%A \r\n" o
let changeCouple() =
print couple;
couple.him <- "Michael Wilding";
print couple;
couple.him <- "Michael Todd";
print couple;
changeCouple()
通过Couple类型的定义可知,him字段是可修改的,就像changeCouple中的代码,但如果尝试修改her的值就会遭遇编译错误。
ref类型
ref类型是一种状态进行修改的简单方式。ref类型其实是包含一个可修改字段的record类型,它定义在F#库中,伴随它的还有两个操作符,它们使得操作ref类型更为方便:
let ref x = { contents = x }
let (:=) x y = x.contents <- y
let (!) x = x.contents
ref“函数”将输入的值“装入”一个记录类型,同时用“:=”操作符来进行赋值,“!”来取值。进一步分析,ref“函数”的类型为a’ -> Ref<a’>,可以了解到“装入”的记录类型为Ref<a’>,由此可知使用了类型参数化(type parameterization),这个概念前面部分已经介绍过了。这意味着ref可以接受任意类型的值,但是一经赋值,其类型也就固定了。Ref<a’>类型暴露了Value属性,我们也可以通过它来获取或设置ref类型的值。
let phrase = ref "Inconsistency"
考虑一个简单的问题,求一个整型数组所有元素的和。先看C#怎么做:
static int TotalArray(int[] array)
{
int total = 0;
foreach (int element in array)
{
total += element;
}
return total;
}
再看看F#的版本:
let totalArray (intArray : int array) =
let x = ref 0
for n in intArray do
x := !x + n
!x
可以看到F#的命令式编程范式下与C#何其相似!
数组(Array)
数组算得上是我们最熟悉的数据结构了。F#中的数组基于BCL中的System.Array类型,是一种可修改的集合类型。数组与列表相对,数组中的值是可修改的,而列表中的值则不能;列表的容量(长度)可以动态增大,数组则不能。一维数组又时被称为向量(Vector),多维数组有时被称为矩阵(Matrix)。定义数组时,将各项置于“[|”和“|]”中,各项间用“;”隔开。
下面的例子演示了如何对数组进行读取和写入操作。
// 定义
let rhymeArray = [| "Hello"; "F#" |]
// 读取
let firstPiggy = rhymeArray.[0]
let secondPiggy = rhymeArray.[1]
// 写入
rhymeArray.[0] <- "Byebye"
rhymeArray.[1] <- "my friend"
// 输出
print_endline firstPiggy
print_endline secondPiggy
print_any rhymeArray
数组跟列表一样,也采用了类型参数化,数组的类型为其元素的类型,因此rhymeArray的类型为string array,也可写作string[]。
F#中的多维数组可分为两类:交错数组(jagged array)和规则数组。交错数组,表示数组的数组,就是说最外部数组的元素也是数组(称为内部数组),内部数组的长度不必相同。而规则数组,事实上整个数组作为一个对象,其内部数组的长度是相同的。
先来看看交错数组的用法:
let jaggedArray = [| [| "one" |]; [| "two"; "three" |] |]
let singleDimension = jaggedArray.[0]
let itemOne = singleDimension.[0]
let itemTwo = jaggedArray.[1].[0]
printfn "%s %s" itemOne itemTwo // one two
jaggedArray的类型为string array array,这也是为什么说它是数组的数组,操作规则数组的语法有所不同:
let square = Array2.create 2 2 0
square.[0,0] <- 1
square.[0,1] <- 2
square.[1,0] <- 3
square.[1,1] <- 4
printf "%A \r\n" square // [| [|1; 2|]; [|3; 4|] |]
square的类型为int[,]。
注意:要编写.NET 1.1 和.NET 2.0兼容的代码,需要使用Microsoft.FSharp.Compatibility命名空间的CompatArray和CompatMatrix类。
数组推导(Array Comprehension)
前面介绍过了关于列表和序列的推导语法。我们也可以使用类似的语法进行数组推导。
let chars = [|'1' .. '9'|]
let squares =
[| for x in 1 .. 9
-> x, x * x |]
printfn "%A" chars
printfn "%A" squares
注意:本文中的代码均在F# 1.9.4.17版本下编写,在F# CTP 1.9.6.0版本下可能不能通过编译。
控制流程(Control Flow)
我们初步了解了F#在函数式编程范式下的控制流程,即if, elif, then, else等组成的结构。在命令式编程范式下,F#提供了更多的控制流程支持,包括if,while和for。
在命令式编程范式下的if结构与函数式编程下对应结构的主要差别在于,对于if分支,调用的函数为unit类型(即无返回值),而且并不要求必须使用else分支:
if System.DateTime.Now.DayOfWeek = System.DayOfWeek.Thursday then
print_endline "Thursday play list: lazy afternoon"
这里print_endline函数的类型为string -> int。尽管else分支不是必须的,但如果需要,你也可以加上,不过else分支也必须为unit类型。
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的经验,那么很容易理解:
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循环结构:
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#很相似,直接看个例子吧:
// 压洲
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结构类似。下面的例子对一个字符串数组进行枚举。
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#类库的方法就变得直白、简单了。先来看看如何使用静态属性和方法:
#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类库的方法做个简单的包装:
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):
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类库中的对象和实例成员
除了类的静态成员,我们也可以创建类的实例并使用它的成员(字段、属性、事件、方法):
#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#提供了两种方式来访问索引器。
#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类型进行匹配,这要用到:?操作符。
#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类型,最后一行的作用在于匹配所有的其它类型。很自然的,在对类型进行匹配时,我们不仅想知道类型,还想了解当前的值,可以这么做:
#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
在前面的文章中我们了解了异常处理的基本用法,这里的技术也可用在异常处理中,因为我们往往会根据类型捕获异常。
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类库时,“|>”操作符很有用,因为它可以帮助编译器正确地推导出函数参数的类型。它的定义很简单:
let (|>) x f = f x
类型信息为:
Tips
'a -> ('a -> 'b) -> 'b
可以这么来理解:x的类型为'a,函数f接受'a类型的参数,返回类型为'b,操作符的结果就是将x传递给f后所求得的值。除了这样将参数“转交”外,“|>”更重要的作用在于帮助编译器进行类型推导:
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的类型:
dateList |> List.iter (fun d -> print_int d.Year)
要了解事情的端倪,我们最好再来看第二个例子:
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#类型上。
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版本下可能不能通过编译。
面向对象编程概述(OOP)
面向对象编程是当今最流行的编程范式,看看TIOBE 2008年9月的编程语言排行榜就很清楚了:
在这些主流语言中,除了C,都或多或少地提供对OOP的支持,而Java和C#更是纯粹的面向对象编程语言,C还有一个子集——Objective-C。值得一提的是Delphi的强势回归。下图则是各个编程范式的占有率:
OOP编程范式是指使用“对象”及“对象”之间的交互来设计应用程序。OOP的基本概念包括类,对象,实例,方法,消息传递,继承,抽象,封装,多态和解耦(Decoupling)等。“一切皆是对象”这句话曾盛极一时,它也衍生出了像设计模式这样的重要理念。关于面向对象编程,需要很多本书来讲述,我这里只想说OOP只是设计方式的一种,计算机要解决的问题包罗万象,面对不同的问题,我们也要选择不同的范式,没必要所有问题都要往OO上靠。要了解OOP的更多内容,可以看这里。
面向对象编程是F#支持的第三种主要范式。我在这里将对其中的基本概念逐一介绍。
类型转换(Casting)
在采用面向对象编程时,我们会面对一个类型的层次结构,在F#中,这个层次结构始自obj(即System.Object)。转换是指改变值的静态类型,可能是向上转换(upcast),将类型向着基类的方向移动,也可能是向下转换(downcast),将类型向着派生类的方向移动。
向上转换是安全的,编译器总能够了解一个类型的所有基类。它的操作符是“:>”。下面的代码将string值转换为obj。
#light
let myStr = "A string value."
let myObj = (myStr :> obj)
向上转换的一个典型应用场景是在定义集合时。如果不使用向上转换,编译器会自动将集合的类型推导为第一个元素的类型,如果其它元素的类型与之不同,就会发生编译错误。我们不得不显式地进行类型转换:
#light
open System.Windows.Forms
let myCtrls =
[| (new Button() :> Control);
(new TextBox() :> Control);
(new Label() :> Control) |]
我们知道在.NET中有值类型和引用类型之分,在对值类型进行向上转换时会自动对其装箱。
向下转换将值的静态类型转换为它的一个派生类,其操作符是“:?>”。这个就没那么安全了,因为编译器没法确定一个类的实例是否与其派生类兼容,如果不兼容(不能转换),程序运行时会抛出一个InvalidCastException。所以使用时要小心一点。看代码吧:
let moreCtrls =
[| (new Button() :> Control);
(new TextBox() :> Control) |]
let control =
let temp = moreCtrls.[0]
temp.Text <- "Click me"
temp
let button =
let temp = (control :?> Button)
temp.DoubleClick.Add(fun e -> MessageBox.Show("Hey") |> ignore)
temp
如果你担心转换的安全性,可以采用模式匹配来代替向下转换。
类型测试
与类型转换相近的概念是类型测试。比如一个窗体类,有时我们会循环它的所有控件,并对Button和TextBox采取不同的处理,这时就需要判断控件的类型。看个简单的例子:
let anotherObj = ("Another string value." :> obj)
if (anotherObj :? string) then
print_endline "This object is a string."
else
print_endline "This object is not a string."
类型转换操作符:>/:?>和类型测试操作符:?,类似于C#的as和is。
对派生类使用类型标注
函数式编程提到过,可以对标识符(比如参数)使用类型标注(Type Annotation),以限定它的类型。但它也有些“死板”,它不考虑类型的继承层次。如果参数类型标注为Control类型,那就不能传入Button类型的值。
#light
open System.Windows.Forms
let showForm(form : Form) =
form.Show()
// PrintPreviewDialog定义在BCL中,派生自Form类
let myForm = new PrintPreviewDialog()
showForm myForm
编译器会报告错误:“This expression has type PrintPreviewDialog but is here used with type Form”。当然,我们可以在传入参数时将值转换为所标注的类:
showForm(myForm :> Form)
这样是可以工作,但毕竟不太漂亮。我们还可以这么做:
let showFormRevised(form : #Form) =
form.Show()
let myForm = new PrintPreviewDialog()
showFormRevised myForm
“#”让代码更优雅,可以避免很多转换。现在重写一下本文开始处的例子:
let myCtrls =
[| (new Button() :> Control);
(new TextBox() :> Control);
(new Label() :> Control) |]
let uc(c : #Control) = c :> Control
let myConciseCtrls =
[| uc(new Button()); uc(new TextBox()); uc(new Label()) |]
使用记录类型模拟对象
记录类型有自己的字段,而字段值可以是函数,这样我们可以使用这个特性来模拟对象的方法。事实上,在函数式编程语言拥有OO结构之前人们就是这样做的。而且,在定义记录类型时,我们仅需要给出函数的类型而无须实现,这样函数的实现就很容易进行替换(而在OOP中,往往需要定义派生类来覆盖基类的实现)。
下面用经典的Shape例子来演示这个过程。
F# Code - 使用记录类型模拟对象
#light
open System
open System.Drawing
open System.Windows.Forms
// 定义两个方法,此时仅仅声明函数的类型
type Shape =
{ reposition : Point -> unit;
draw : Graphics -> unit }
// 创建一个Shape实例,并为它实现了两个方法
let makeShape initPos draw =
let currPos = ref initPos in
{ reposition = (fun newPos -> currPos := newPos);
draw = (fun g -> draw !currPos g);
}
// 创建一个circle(Shape)实例,它替换了前面draw的实现
let makeCircle initPos diam =
makeShape initPos (fun pos g ->
g.DrawEllipse(Pens.Blue, pos.X, pos.Y, diam, diam))
// 创建一个square(Shape)实例,它替换了前面draw的实现
let makeSquare initPos size =
makeShape initPos (fun pos g ->
g.DrawRectangle(Pens.Blue, pos.X, pos.Y, size, size))
let getPoint(x, y) = new Point(x, y)
let shapes =
[ makeCircle (getPoint(10, 10)) 20; makeSquare (getPoint(30, 30)) 20; ]
let mainForm =
let form = new Form()
let rand = new Random()
form.Paint.Add(fun e ->
shapes |> List.iter(fun s -> s.draw e.Graphics)
)
form.Click.Add(fun e ->
shapes |> List.iter(fun s ->
s.reposition(new Point(rand.Next(form.Width), rand.Next(form.Height)));
form.Invalidate())
)
form
[<STAThread>]
do Application.Run(mainForm)
这段代码将在窗体上画出一个圆和正方形。makeShape用于返回一个通用的Shape,makeCircle和makeSquare则用于返回两种特定的Shape。最后在Form的Paint事件中画出这两个Shape。这样我们可以快速创建不同功能的记录类型,却不用创建额外的类型。这与我们在C#中的惯用方式不同,下一节中将介绍一种更为自然的方式:向F#类型中添加成员。
向F#类型添加成员
F#中的类型包括记录(Record)和Union类型,两者均可以添加成员。在函数式编程(下)中,我们看到了如何定义类型,要为之添加成员需要在字段定义的末尾处进行。看下面的例子:
#light
// 包含两个字段,一个方法
type Point =
{ mutable top : int;
mutable left : int }
with
member this.Swap() =
let temp = this.top
this.top <- this.left
this.left <- temp
end
let printAnyNewline x =
print_any x
print_newline()
// 定义Point类的实例
let p = { top = 30; left = 40; }
let main() =
printAnyNewline p
p.Swap()
printAnyNewline p
main()
输出结果为:
{ top = 30;
left = 40;}
{ top = 40;
left = 30;}
看看Point的定义,前半部分是字段们的定义,这个跟前面的一样,后半部分是一个with…end代码块,这里通过member关键字定义了方法Swap。注意Swap前面的this参数,它表示持有该方法的类实例,即Swap通过这个实例被调用。一些语言都有特定的关键字来表示这里的this,如C#中的this和VB.NET中的Me,但F#要求你选择该参数的名字,该名字没有限制,你完全可以用x代替这里的this。只是如果用惯了C#,this看起来会更亲切。
Union类型也可以有成员方法。定义方式与记录类型相同。
#light
type DrinkAmount =
| Coffee of int
| Tea of int
| Water of int
with
override this.ToString() =
match this with
| Coffee x -> Printf.sprintf "Coffee: %i" x
| Tea x -> Printf.sprintf "Tea: %i" x
| Water x -> Printf.sprintf "Water: %i" x
end
let t = Tea 2
print_endline (t.ToString())
输出结果为:
Tea: 2
这里为类型添加了成员方法ToString,不过使用的是override而不是member,这意味着覆盖了基类(obj)的ToString实现。
小结
首先对OOP做了简单介绍,然后逐一介绍了类型转换、类型测试、对派生类使用类型标注、使用记录类型模拟对象、向F#类型添加成员方法,通过这些我们能将值和函数封装在类型内部。在下一篇中将介绍接口和继承等相关语言结构。
注意:本文中的代码均在F# 1.9.4.17版本下编写,在F# CTP 1.9.6.0版本下可能不能通过编译。
对象表达式(Object Expressions)
F#中的OOP语法很简洁,而对象表达式则正是这种简洁性的核心所在。通过对象表达式,我们可以创建抽象类或接口的轻量级实现,也可以对一个具体的类进行继承。换言之,可以在实现抽象类或接口,或者继承具体类的同时创建新类型的一个实例。下面来看如何对泛型接口IComparer<’T>应用对象表达式。
#light
open System
open System.Collections.Generic
let revStringComparer =
{ new IComparer<string>
with
Compare(s1, s2) =
let rev (s : string) =
new string(Array.rev(s.ToCharArray()))
let reversed = rev s1
reversed.CompareTo(rev s2)
}
let winners = [| "Sandie Shaw"; "Bucks Fizz"; "Dana International"; "Abba" |]
print_any winners
print_newline()
Array.Sort(winners, revStringComparer)
print_any winners
运行结果为
[|"Sandie Shaw"; "Bucks Fizz"; "Dana International"; "Abba"|]
[|"Abba"; "Dana International"; "Sandie Shaw"; "Bucks Fizz"|]
这里演示了实现IComparer的过程,该接口有一个方法Compare,它接受两个参数并通过返回值来表示参数比较的结果。它是泛型接口,这里的类型参数为string,可以在标识符revStringComparer定义的第二行看到。从标识符的名字也可以了解到,它是将参数颠倒后进行比较,运行结果的第二行印证了这一点。可以看到“在实现接口的同时返回一个实例”。
看看多重继承的情况。在C#中,一个类不能继承多个类,却可以同时实现多个接口,F#也是一样的。要注意的是,如果同时继承类并实现接口,须将类的部分房子最前面;而且必须在第一个类/接口定义完毕之后才能开始第二个接口的实现。
多重继承的例子
#light
open System
open System.Drawing
open System.Windows.Forms
let makeNumberControl(n: int) =
{ new Control(Tag = n, Width = 32, Height = 16)
with
override x.OnPaint(e) =
let font = new Font(FontFamily.Families.[1], 12.0F)
e.Graphics.DrawString(n.ToString(), font, Brushes.Black,
new PointF(0.0F, 0.0F))
interface IComparable
with
CompareTo(other) =
let otherCtrl = other :?> Control in
let n1 = otherCtrl.Tag :?> int in
n.CompareTo(n1) }
let numberCtrls =
let temp = new ResizeArray<Control>()
let random = new Random()
for index = 1 to 10 do
temp.Add(makeNumberControl(random.Next(100)))
temp.Sort()
let height = ref 0
temp |> Seq.iter
(fun c ->
c.Top <- !height
height := c.Height + !height)
temp.ToArray()
let myForm =
let temp = new Form() in
temp.Controls.AddRange(numberCtrls);
temp
[<STAThread>]
do Application.Run(myForm)
在对象表达式makeNumberControl中,它继承了Control类,同时也实现了IComparable接口。这里可以看到,在CompareTo方法中根据控件的Tag值进行比较,在调用Sort方法(ResizeArray即泛型类List<’T>)时会根据Compare方法对控件进行排序。排序完成后,依次呈现这些控件,如下图:
对象表达式是一种强大的机制,可以帮助我们快速而简洁地将非F#库中的对象引入F#代码。它的缺点则是没法添加额外的属性或方法。这也是在前面的例子中为何使用Tag属性来存放n的值。
定义接口
接口仅包含抽象的方法和属性。它为所有实现它的类定义了一份“契约”。F#中接口的概念与C#中的相同,在此不再赘述。如:
type MyInterface = interface
abstract ChangeState : int -> unit
end
实现接口
实现接口的语法也很简单。仍以上面的接口为例:
F# Code - 实现接口
type MyImpl = class
val mutable state : int
new() = { state = 0 }
interface MyInterface with
member x.ChangeState y = x.state <- y
end
end
let impl = new MyImpl()
let inter = impl :> MyInterface
let printIntAndNewLine i =
print_int i
print_newline()
let main() =
inter.ChangeState 1
printIntAndNewLine impl.state
inter.ChangeState 2
printIntAndNewLine impl.state
main()
运行结果为:
1
2
不知你有没有注意到,在调用ChangeState方法前,我们先将impl转换为了MyInterface类型,否则不能调用。这是因为接口在F#中是显式实现的,如果希望不经转换就可以直接调用接口的方法,只能在类中在显式地添加这个方法:-(
类、字段和显式的构造函数
前面我们主要还是使用非F#库中的类,现在来看看如何定义自己的类。
#light
type EmptyClass = class
end
let emptyItem = new EmptyClass()
嗯,很明显,这里试图定义一个类,然后创建它的一个实例。不过编译器不允许你这么做,它会告诉你:“error FS0191: No constructors are available for the type 'EmptyClass'”。没有构造函数?如果你是C#程序员就会觉得奇怪了。事实上,F#没有提供默认的构造函数这种机制,我们必须得手工添加一个,构造函数的名字总是为new。
type EmptyClass = class
new() = {}
end
另外,默认的构造函数容易使得字段不能正确地初始化,会给程序带来隐患,而在F#中的构造函数必须初始化所有字段。使用val关键字定义字段,在下面的类MyFile中,它拥有两个字段,path和innerFile,两个字段在构造函数内进行初始化。
#light
open System.IO
type MyFile = class
val path : string
val innerFile : FileInfo
new() = new MyFile("default.txt")
new(p) =
{ path = p;
innerFile = new FileInfo(p) }
end
我们还可以看到,这个类有两个构造函数,也就是说构造函数可以重载。观察构造函数new(p),它有一部分在{}内,这个代码块称为构造函数的初始化块,在这里唯一能做的事情就是初始化字段。如果想做更多的事情,就要在后面加then添加其它代码:
type MyFile2 = class
val path : string
val innerFile : FileInfo
new(p) as x =
{ path = p;
innerFile = new FileInfo(p) }
then
if not x.innerFile.Exists then
let textFile = x.innerFile.CreateText()
textFile.Dispose()
end
new(p)后面加了as x,这样就可以在后面的代码中引用当前的对象了。then后面的代码首先检查文件是否存在,如果不存在就创建一个新文件。
注意:上面两个类MyFile和MyFile2中的字段都是只读的,如果需要修改它们,可以在字段名字前面添加关键字mutable,如val mutable innerFile;同时它们的访问修饰符都是public,在下一篇文章将介绍如何在类中定义属性。
思考:在函数式编程中,曾介绍过自定义的记录类型,比如
type couple = { him : string; her : string }
那么这里的couple类和上面的MyFile类主要有哪些区别呢?请出我们的老朋友.NET Reflector来吧。在Reflector中打开编译过的F#程序集可以看到,couple的类型定义为:
Type Infomation
public sealed class couple : IStructuralHash, IComparable
这是一个sealed类,这意味着无法继承它,其中的her和him都是只读属性。
而MyFile的定义则是:
Type Infomation
public class MyFile
这个就跟C#中常规类的定义一致了,其中的path和innerFile都是只读属性。
隐式的类构造(Implicit Class Construction)
除了上面的显式构造函数,F#还支持隐式的类构造语法,这样的语法更为紧凑。它允许在定义类的成员前执行一系列的let值绑定语句,这些绑定属于类的私有部分。
F# Code - 隐式的类构造
#light
open System.IO
type MyOtherFile(path) = class
let innerFile = new FileInfo(path)
member x.InnerFile = innerFile
end
我觉得对于前面的(显式的类构造)MyFile类定义,这里的MyOtherFile类当然更为紧凑,看起来在定义类的同时就定义了构造函数,随后马上初始化了字段,不过这里的innerFile已经是私有的了,所以再添加一个属性InnerFile来公开innerFile字段。
隐式的类构造要比等价的显式类构造代码少很多,但是有时必须要用显式的类构造,比如编写拥有多个构造函数的类的时候。
小结
本文首先介绍了强大的对象表达式机制,通过它,我们可以快速地创建抽象类或接口的轻量级实现;接下来是定义和实现接口;最后介绍了如何创建和实例化一个类,在创建类实例的时候,我们既可以采用显式的构造函数,也可以采用更为紧凑的“隐式的类构造”机制。
注意:本文中的代码均在F# 1.9.4.17版本下编写,在F# CTP 1.9.6.0版本下可能不能通过编译。
类的继承
在前面面向对象介绍过的对象表达式中,已经涉及到了类的继承,我们可以覆盖类的成员实现,然后创建新类的实例。这一节里,我们来看看常规的继承机制,熟悉C#的开发人员会感到更亲切。
F# Code - 类的继承
#light
type Base = class
val state : int
new() = { state = 0}
end
type Sub = class
inherit Base
val otherState : int
new() = { otherState = 1 }
end
let myObj = new Sub()
printfn "myObj.state = %i, myObj.otherState = %i"
myObj.state
myObj.otherState
System.Console.Read()
运行结果为:
myObj.state = 0, myObj.otherState = 1
这里Base类为基类,有一个字段state,Sub继承了它,并有一个新字段otherState,下面的代码可以看到,此时Sub类继承了Base类的state字段。需要注意的是,这里的Base必须要有一个无参的构造函数,否则不能通过编译。其原因是,跟C#一样,在初始化派生类时会调用基类的构造函数,而F#中类没有默认的构造函数,所以必须显式添加构造函数。而如果基类的构造函数带有参数的话,派生类的构造函数写法也有所不同。
F# Code - 基类具有含参的构造函数
#light
type Base = class
val state : int
new(st) =
{ state = st}
then
printfn "Init base class"
end
type Sub = class
inherit Base
val otherState : int
new() =
{ inherit Base(0)
otherState = 1 }
then
printfn "Init sub class"
end
let myObj = new Sub()
printfn "myObj.state = %i, myObj.otherState = %i"
myObj.state
myObj.otherState
System.Console.Read()
运行结果:
Init base class
Init sub class
myObj.state = 0, myObj.otherState = 1
这个结果说明确实调用了基类的构造函数。Inherit Base(0)则说明,我们必须选择正确的构造函数。
类的方法
定义方法时可以使用4个相关的关键字,即member、override、abstract、default。在C#中,类的方法大体上可分为三种情况。一是在基类中定义,派生类中不能覆盖的;二是在基类中定义,派生类可以覆盖的(使用virtual关键字);三是抽象方法(或接口方法),派生类需要提供实现的(除非派生类也是抽象类或接口)。对于情况一,可以简单地使用member关键字;情况二,可同时使用abstract和default(或override),abstract说明该方法可以进行覆盖,default则提供了默认实现;情况三,可仅仅使用abstract,同时为类添加特性AbstractClass,说明该方法是抽象类的一个抽象方法,必须在派生类中提供实现。
F# Code - 类继承时的方法
#light
type Base = class
val mutable state: int
new() = { state = 0 }
member x.JiggleState y = x.state <- y
abstract WiggleState: int -> unit
default x.WiggleState y = x.state <- y + x.state
end
type Sub = class
inherit Base
new() = {}
override x.WiggleState y = x.state <- y &&& x.state
end
let myBase = new Base()
let mySub = new Sub()
let test(c: #Base) =
c.JiggleState 1
print_int c.state
print_newline()
c.WiggleState 3
print_int c.state
print_newline()
print_endline "base class: "
test myBase
print_endline "sub class: "
test mySub
运行结果为:
base class:
1
4
sub class:
1
1
可以看到JiggleState方法继承了Base的实现,而WiggleState则覆盖了Base的默认实现。
访问基类的成员
如果你曾写过Page类的派生类,那么很可能写过base.OnLoad()这样的代码。“base”用来引用基类中的成员,在F#中稍有不同。就像没有this一样,base也是没有的,需要给它手工指定名称。
F# Code - 访问基类的成员
#light
open System
open System.Drawing
open System.Windows.Forms
type RectangleForm(color) = class
inherit Form() as base
override x.OnPaint(e) =
e.Graphics.DrawRectangle(color, 10, 10,
x.Width - 30, x.Height - 50)
base.OnPaint(e)
override x.OnResize(e) =
x.Invalidate()
base.OnResize(e)
end
let form = new RectangleForm(Pens.Blue)
[<STAThread>]
do Application.Run(form)
这里定义Form类的派生类RectangleForm,注意在inherit后面的as base,这里的base就是给基类起的名字,这个名字是任意的。在覆盖OnPaint时就调用了基类的OnPaint方法。
属性和索引器
属性(Property)和索引器(Indexer)是.NET中重要的“语法糖”特性,它们使得我们的代码更为直观、简洁,而它们本质上是方法。
F# Code - 定义属性
#light
open System
type Propreties() = class
let mutable rand = new Random()
member x.MyProp
with get() = rand.Next()
and set y = rand <- new Random(y)
end
let prop = new Propreties()
prop.MyProp <- 10
printfn "%d" prop.MyProp
printfn "%d" prop.MyProp
在Properties类中定义属性MyProp,这里我们可以看到熟悉的get/set变成了两个方法get/set,参数y就是C#中的value。
索引器是一种特殊的属性(又称含参属性)。在C#中,索引器本质上是名为Item的方法。在F#中,我们也可以使用Item之外的名字。
F# Code - 定义索引器
#light
type Indexers(vals : string[]) = class
member x.Item
with get(i) = vals.[i]
and set(i, v) = vals.[i] <- v
member x.MyString
with get(i) = vals.[i]
and set(i, v) = vals.[i] <- v
end
let indexer = new Indexers [| "One"; "Two"; "Three"; "Four" |]
indexer.[0] <- "Five"
indexer.Item(2) <- "Six"
indexer.MyString(3) <- "Seven"
print_endline indexer.[0]
print_endline(indexer.Item(1))
print_endline(indexer.MyString(2))
print_endline(indexer.MyString(3))
这里的Indexers类定义了两个索引器:Item和MyString,通过Reflector可以看到,它们的签名相同,都是“public string this[int i] { get; set; }”,在C#中这是不允许的。C#中索引器的默认名称为Item,对于这里的Item来说,可以用两种方式访问它,而MyString就只有一种了。如果需要考虑同其它语言的兼容性,建议使用Item定义属性。
类的静态方法
静态方法的定义是实例方法类似,不过要用到static关键字。访问静态方法时,不需要建立类的实例,可以通过类直接访问,如下面的ReverseString方法。
F# Code - 定义静态方法
#light
type MyClass = class
static member ReverseString(s: string) =
let chars = s.ToCharArray()
let reversedChars = Array.rev chars
new string(reversedChars)
end
let myString = MyClass.ReverseString "dlrow olleH"
print_string myString
运行结果为:
Output
Hello world
定义委托
在C#中,通过委托可以像“值”那样去处理方法。显然,在F#中不需要这样,因为函数本来就是当作值来看待的。但是考虑到与其它语言的交互,有时也需要定义委托。
F# Code - 定义委托
#light
type MyDelegate = delegate of int -> unit
let myInst = new MyDelegate(fun i -> print_int i)
let intList = [1; 2; 3]
intList |> List.iter(fun i -> myInst.Invoke(i))
这里myInst是委托MyDelegate的一个实例,我们通过Invoke来调用它。运行结果为:
Output
123
定义结构类型
结构(struct)与类(class)的区别就是我们常说的值类型和引用类型的区别,在此不再赘述。下面定义的结构IpAddress用于表示IP地址。
F# Code - 定义结构类型
#light
type IpAddress = struct
val first: byte
val second: byte
val third: byte
val fourth: byte
new(first, second, third, fourth) =
{ first = first;
second = second;
third = third;
fourth = fourth }
override x.ToString() =
Printf.sprintf "%O.%O.%O.%O" x.first x.second x.third x.fourth
member x.GetBytes() = x.first, x.second, x.third, x.fourth
end
定义枚举类型
枚举类型定义了一组符号名称和数值对,本质上讲,枚举类型就是定义了一组常数字段的结构。F#中枚举的定义也是很简单的。
F# Code - 定义枚举类型
#light
type Season =
| Spring
| Summer
| Autumn
| Winter
如果你希望定义位标记枚举,可以使用System.Flags特性。
小结
至此,F#中的面向对象编程范式介绍完毕,我们手中的F#也变得更为锐利。本文首先介绍了类的继承、类的方法(虚方法、抽象方法等)、访问基类等跟继承相关的概念;接着是类的属性和索引器、类的静态方法这些类的特殊成员;最后讨论了如何定义委托、结构类型、枚举类型等特殊类型。相信有了这些知识,我们完全可以使用F#代替C#来编写类库了。学习这些知识的过程,也给了我们一个从新的角度了解.NET Framework的机会。在学习了F#的三种主要编程范式之后,下一步该考虑如何在实战中应用它,比如如何组织规模较大的程序,如何建立UI,如何与其它.NET语言进行交互等等,在后续的随笔中将逐步介绍这些内容。
注意:本文中的代码均在F# 1.9.4.17版本下编写,在F# CTP 1.9.6.0版本下可能不能通过编译。
关于函数式编程(Functional programming,FP)
函数式编程(FP)是一种编程范式,它将计算过程视为函数运算,避免状态和数据的修改。与命令式编程相比,它更强调函数的运用。λ运算构建了函数式编程的基础。重要的函数式编程语言包括Lisp、Scheme、Erlang、Haskell、ML、OCaml等,微软则在2005年引入了F#。
此外,包括C/C++/C#/Python/Javascript等很多语言也提供了对FP的部分支持。由此我们可以得出一个结论,随着现实问题复杂度的增加,单一的编程范式很难满足需要了。我们需要对FP有更多的了解,问题是学习哪种语言呢?作为一个.NET程序员,我的答案是F#。使用F#,除了能借助FP的力量,最重要的一点是它跟.NET平台的无缝兼容,它可以轻松地与C#或VB.NET进行互操作,通过F#,我们手中的C#/VB.NET会变得更为强大。
本文尝试通过F#对FP的一些重要特征和属性做些介绍,包括函数(一等公民、高阶函数、柯里化、匿名函数、闭包)、避免副作用(对状态和数据的修改)、递归、惰性求值、模式匹配;然后讨论了FP对代码结构的影响。像Continuation和Monad留在以后的随笔中介绍。希望能增加您对FP的认识。
函数是一等公民(First-class citizen)
这里的citizen也可换作object/value/entity,所谓一等公民是指那些在程序中可以无限制(相比于同一语言中的其它对象)使用的对象。在编程语言中,“函数是一等公民”意味着它可以:
1. 表示为匿名的文字值
2. 存储于变量中
3. 存储于数据结构中
4. 作为函数的参数进行传递
5. 作为函数的返回值
6. 在运行时进行构造
F#中的函数是一等公民,而在C#中,函数不是一等公民,比如我们不能把函数作为参数进行传递,也不能将其作为返回值,而对类则可以这么做。这种不同并不值得奇怪。如果我们把人类社会作为一个抽象来看,那么在它的不同实现中公民的等级也有所不同。在缅甸,和尚是一等公民,男人是二等公民,女人和尼姑是三等公民,人妖是四等公民,我们国家显然不是这样,但缅甸和中国的公民们大部分都能活得好好的。
F# Code - First-class citizen
#light
let makeDerivative f (deltaX: float) =
fun x -> (f(x + deltaX) - f(x)) / deltaX
let cos = makeDerivative sin 0.000001
open System
let writeLine input =
print_any input
Console.WriteLine()
writeLine (cos 0.0) // ~= 1
writeLine (cos(Math.PI / 2.0)) // ~= 0
Console.Read()
在这个例子中,makeDerivative函数的第一个参数f是一个函数,它的返回值也是函数,返回的是一个匿名函数。
高阶函数(High-level function)
高阶函数是指那些可以接受其它函数为参数,或者把函数作为返回值的函数。上面的makeDerivative函数就是一个例子。高阶函数描述的是函数的数学概念,而“函数是一等公民”则是一个计算机科学的术语。
还记得在高中数学中学过的复合函数的概念吗?如果u(x) = x * 2,而y(u) = u + 3,那么y接受的“参数”是一个函数,而y本身也是一个函数。
函数柯里化(Currying)
所谓柯里化,简单来说是指对于一个接受多个参数的函数,将第一个参数设为一个固定值,这样会得到一个新函数,新函数的参数是原函数第一个参数之外的函数。看下面简单的例子:
F# Code - 函数柯里化
// val add : int -> int -> int
let add a b = a + b
// val increment : (int -> int)
let increment = add 1
函数add接受两个参数,我们将第一个参数a设为固定值1,就得到新函数increment。
匿名函数(Anonymous function)
顾名思义,我们定义了一个函数,也可以调用它,但没有为它设定一个名称,这样的函数就是匿名函数。在lambda运算中,所有函数都是匿名函数。
在F#的列表操作中,会经常用到匿名函数。
F# Code - 匿名函数
List.filter (fun i -> i % 2 = 0) [1 .. 20]
List.filter函数用于对列表进行过滤,其签名为:
Type Infomation
val it : (('a -> bool) -> 'a list -> 'a list)
第一个参数是返回bool值的函数,第二个参数是列表,返回值为使得第一个参数返回true的那些元素组成的新列表。本例中filter的第一个参数即匿名函数,使用关键字fun进行定义。本例中过滤后的新列表为:
[2; 4; 6; 8; 10; 12; 14; 16; 18; 20]
此外还可以使用function关键字定义匿名函数,第二种方式还可以使用模式匹配。
闭包(Closure)
闭包是个比较抽象的概念,先来看下面的例子吧。
F# Code - 闭包(针对宿主函数的参数)
#light
open System
let makePower power =
let powerFn base = Math.Pow(base, power)
powerFn
let square = makePower 2.0
Console.WriteLine(square(3.0))
运行结果为9。我们来分析一下。makePower函数接受参数power,在其内部定义了一个函数powerFn,它接受参数base,并使用到了power的值,makePower函数将powerFn作为它的返回值。那么square也是一个函数,它相当于Math.Pow(base, power),这里的power值为2.0。问题是power不在powerFn的作用域内,调用makePower结束后,它的就不复存在,那么square到哪里去找power的值呢?如果我们这样创建一个新的函数来求数的立方值:
F# Code
let cube = makePower 3.0
那么运行时就要存储两个power的拷贝了。不仅这样,每个我们用makePower创建的函数都要使用power的一个拷贝,保存这些值的现象称为闭包。
上面的闭包保存了宿主函数的参数值。另外闭包还可以保存宿主函数的局部变量:
F# Code - 闭包(针对宿主函数的局部值)
let makeIncrementer() =
let n = ref 0
let increment() =
n := !n + 1
!n
increment
let inc1 = makeIncrementer()
let inc2 = makeIncrementer()
Console.WriteLine(inc1()) // 1
Console.WriteLine(inc1()) // 2
Console.WriteLine(inc1()) // 3
Console.WriteLine(inc2()) // 1
Console.WriteLine(inc2()) // 2
Console.WriteLine(inc2()) // 3
这里闭包为increment保存了n的值,n是ref值,所以是可以修改的。局部变量的生命周期不再由简单的作用域来限定了,我们可以得出结论,它们不再保存在堆栈上——而是必须保存在堆上。闭包使得包内的函数可以访问作用域之外的值,当闭包应用了一个不在其作用域的值时,它会在其宿主作用域中查找。想一想,上面的makeIncrementer、n还有increment,这个小小的封闭体是不是很像面向对象中的类呢?
没有副作用(Side effect)
如果一个函数或表达式改变了某个状态,我们就说该函数或表达式产生了副作用。比如一个函数可能会修改全局/静态变量、参数,写文件,输出到控制台,或者调用其它产生副作用的函数。在纯粹的函数式编程中,函数没有副作用。
如果没有副作用,我们的程序还能干什么?在考虑这个问题前,先来想想,没有副作用后,程序会变成什么样子。此时,唯一影响函数返回值的是参数,这样对于相同的参数,它会返回相同的值,而且它对外部状态毫无影响。既然返回值与外部状态无关,单元测试时只要考虑参数就好了;在调试时则只需检查调用堆栈里的参数;如果两个函数没有数据的依赖,就不必考虑它们的调用顺序;函数可以轻松地并行执行,这里没有死锁;编译器可以调整或合并表达式的求值,比如用在惰性求值这里。
没有副作用,程序的状态该如何保存呢?把函数提升为一等公民了,它就该多做点事情,我们要把状态保存在参数中。如果要保存某个状态一段时间并时不时地对其进行一些修改,可以写个递归函数。
递归(Recursion)
递归是编程中的一个非常重要的概念,它表示函数通过自身进行定义,亦即在定义处调用自身。在函数式编程中常用于表达命令式编程的循环。下面是求阶乘的函数:
F# Code - 递归
let rec factorial x =
match x with
| x when x < 0 -> failwith "value must be greater than or equal to 0"
| 0 -> 1
| x -> x * factorial(x - 1)
使用rec关键字定义递归函数,这里的match表示模式匹配结构。
惰性求值(Lazy evaluation)
惰性求值又称延迟求值(Delayed evaluation),它将运算时间推迟到真正要使用运算结果的时候。
在惰性求值之前,我还遇到过两个懒惰的家伙。一个是Lazy load,这个在ORM中是常见的概念:
XML Code - iBATIS.NET Lazyload
<resultMap class="User" id="User_Result">
<result column="UserId" property="UserId" />
<result column="Password" property="Password" />
<result column="UserId" property="RoleList" select="Role_SelectByUserId" lazyLoad="true" />
</resultMap>
这是iBATIS.NET中的一段配置,第三个result节点的lazyLoad特性值为true,这意味着对于User类的RoleList属性来说,只有在用到它的时候才会执行SQL语句进行加载。
另一个是Lazy initialization,Singleton模式的一种实现方式用到了它:
C# Code - Singleton模式
public class Singleton
{
protected Singleton()
{
}
// Return an instance of Singleton
public static Singleton Instance
{
get { return SingletonCreator.Instance; }
}
private sealed class SingletonCreator
{
// Retrieve a single instance of a Singleton
private static readonly Singleton _instance = new Singleton();
// Return an instance of the class
public static Singleton Instance
{
get { return _instance; }
}
}
}
在F#中,如果要利用延迟求值的特性,必须要显式地声明哪些表达式的求值需要延迟,这个要使用lazy关键字。如果需要对该表达式求值,则要调用Lazy模块的force函数。在调用force函数的时候,它会计算表达式的值,而所求得的值会被缓存起来,再次对表达式应用force函数时,所得的值其实是缓存中的值。
F# Code - 惰性求值
let sixtyWithSideEffect = lazy(printfn "Hello, sixty!"; 30 + 30)
print_endline "Force value the first time:"
let actualValue1 = Lazy.force sixtyWithSideEffect
print_endline "Force value the second time:"
let actualValue2 = Lazy.force sixtyWithSideEffect
运行结果为:
Output
Force value the first time:
Hello, sixty!
Force value the second time:
惰性求值可以减少不必要的运算,从而带来性能上的提升;也可用于构造无穷的数据结构(如自然数序列)。
此外,我在下午1到4点还会听HitFM的Lazy afternoon :-)
模式匹配(Pattern matching)
模式匹配不是什么新的创新的特性。事实上,它和函数式编程的关系不大。把产生模式匹配归因于函数式编程的唯一的原因是函数式语言一度提供了模式匹配,然而现在的命令式语言还做不到。模式匹配是指对于一个数据结构,检查其是否包含匹配给定模式的元素。正则表达式就是一种典型的模式匹配应用,它用于检查字符序列。
在F#中,模式匹配允许你根据标识符值的不同进行不同的运算。有点像一连串的if...else结构,也像C#中的switch,但是它更为强大和灵活。看下面Lucas序列的例子,Lucas序列定义跟Fibonacci序列一样,只不过起始值不同:
F# Code - Lucas数
let rec luc x =
match x with
| x when x <= 0 -> failwith "value must be greater than zero"
| 1 -> 1
| 2 -> 3
| x -> luc(x - 1) + luc(x - 2)
这里匹配的对象是x,它是一个整数,除了对基元类型匹配外,还可以对复杂类型进行匹配,下面的例子是对元组进行匹配:
F# Code - 对元组应用模式匹配
let myOr b1 b2 =
match b1, b2 with
| true, _ -> true
| _, true -> true
| _ -> false
而模式匹配的常见用法是对列表进行匹配:
F# Code - 对列表应用模式匹配
let rec concatenateList list =
match list with
| head :: tail -> head @ (concatenateList tail)
| [] -> []
这里的concatenateList函数可将列表的列表拼接为一个列表。
考虑到F#跟.NET平台的亲密关系,我们还可以对.NET类型进行匹配。
F# Code - 对.NET类型应用模式匹配
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"
看到模式匹配有多么灵活和强大了吧?
FP对代码结构的影响
这里将从程序架构、类/接口/函数的组织、函数(或方法)的实现这三个层次来讨论。
在程序架构上,FP对代码结构的影响最小,因为此时问题域本身是最重要的,我们得更多地关注层次较高的内容,比如性能、可靠性等。
在类/接口/函数的组织这个层次上,OO的设计仍然不错,它可以较好地分解问题域并构建解决方案。但是在这个层次上OO有些情况下也不是那么奏效了,比如Command模式。Command模式往往表现为仅包含单一方法(比如Do或Execute)的类/接口,这只是穿上了“类”的外衣的函数。Visitor模式亦是如此。可以说在FP中,有些模式已经内置在语言中了!
在函数(或方法)的实现这个层次上,FP的影响最大。此时没有“变量”了;不再需要类型注解了;控制结构表现为表达式和递归;可以定义局部/嵌套的函数……
列表(List)是函数式编程(FP)的基础。事实上,FP的重要代表Lisp的名字即源自“List Processing”,它的发明者John McCarthy于1960年发表的论文向我们展示了,在只给定几个简单的操作符和一个表示函数的记号的基础上,如何构造出一个完整的编程语言,他的主要思想之一是用一种简单的数据结构列表来表示代码和数据。
链表(Linked list)是Lisp的主要数据结构之一,并且Lisp的源代码本身也由列表构成。F#中的列表类型表示为链表,它与C#中的数组、泛型List<T>类型有着明显的不同。链表可以用下面的图表示:
首先我们来看一下FP中列表的基本操作(其中的代码都由F#实现)。
列表的基本操作
cons:它是“construct”的缩写,用于构造列表,意即将一个元素添加到列表的开头。我们先约定空表表示为[],在此基础上再约定操作符“::”表示cons操作,这样我们就可以构造任意的列表了。如:
F# Code - 列表的cons操作
let emptyList = [] // []
let oneItem = 3 :: [] // [3]
let twoItems = 2 :: oneItem // [2; 3]
let threeItems = 1 :: twoItems // [1; 2; 3]
可以看到这里是如何通过“cons”操作来一步一步构造列表的。
car:它表示“Contents of the Address part of the Register”,意即列表的第一个元素。F#中使用List模块的hd(Head)函数来执行car操作:
F# Code - 列表的car操作
let stringList = ["No "; "one "; "really "; "listens to "; "anyone else."]
List.hd stringList // "No "
cdr:它表示“Contents of the Decrement part of the Register”,意即列表中第一个元素之外的元素。F#中使用List模块的tl(Tail)函数来执行cdr操作:
F# Code - 列表的cdr操作
let stringList = ["No "; "one "; "really "; "listens to "; "anyone else."]
List.tl stringList // ["one "; "really "; "listens to "; "anyone else."]
有了这三种基本操作,其它的操作都可以推导出来了。比如:
concat:该操作用于连接两个列表。在F#用“@”操作符执行该操作。
let list1 = [2; 3; 4]
let list2 = [5; 6; 7]
let largeList = list1 @ list2
print_any largeList // [2; 3; 4; 5; 6; 7]
length:该检查列表的元素数量,在F#中使用List模块的length函数:
let list1 = [2; 3; 4]
List.length list1 // 3
nth:该操作返回列表的第n个元素,在F#中使用List模块的nth函数:
let list1 = [2; 3; 4]
List.nth list1 2 // 4
这里代码用来获取list1中的索引(基于0)为2的元素,返回4。
现在再来看看List模块还有哪些重要的函数:
List模块(Microsoft.FSharp.Collections.List)的函数
List.rev:很明显,它可以翻转一个列表。要注意的是该函数会创建整个列表的一个副本,所以要注意性能问题。
List.zip:该函数的签名为a’ list -> b’ list -> (a’ * b’) list,将两个列表打包为一个元组的列表:
print_any(List.zip [1; 2] ["one"; "two"]) // [(1, "one"); (2, "two")]
List.exists:该函数的签名类型为(a’ -> bool) -> a’ list -> ‘a,顾名思义,它用于检查列表是否包含了满足指定谓词函数的元素。
List.find:该函数的签名类型为(a’ -> bool) -> a’ list -> ‘a,可以看到它接受两个参数,第一个参数是谓词函数,第二个参数及传入的列表。可以这么理解,find函数对列表的元素逐一检查,看是否满足上面所说的谓词函数,如果找到了,返回该元素的值,否则抛出异常。
let result = List.find (fun i -> i * i = 64) [1..10]
print_int result // 8
这里检查[1..10]中的每个数字,返回8。但如果找不到任何元素满足的话,会抛出KeyNotFoundException,这时可以使用tryfind,这个类似于C#中TryParse方法。
List.filter:该函数接受的参数与find函数类似,不过它的功能是对列表的元素进行过滤,将所有满足谓词函数的元素构造为一个列表返回:
let list3 = List.filter (fun i -> i % 2 = 0) [1..20]
print_any list3 // [2; 4; 6; 8; 10; 12; 14; 16; 18; 20]
另外,还有功能强大的聚合函数(Aggregate Operators),即iter、map和fold。(事实上,F#中的Set、Seq、Option和Array模块都支持这三种操作)
List.iter:该函数将枚举列表中的每个元素,并将每个元素应用于指定的函数,如:
List.iter (fun i -> printfn "List contains %d" i) [1..5]
输出结果为:
List contains 1
List contains 2
List contains 3
List contains 4
List contains 5
List.map:map函数用将列表转换为另一个列表。它的签名类型为:
Type Infomation
(‘a –> ‘b) –> ‘a list –> ‘b list
看看这个效果图就容易理解了,对第一个列表的元素逐一应用函数,从而得到一个新的列表:
let x = List.map (fun i -> i * (-1)) [1..5]
printfn "%A" x // [-1; -2; -3; -4; -5]
List.fold:在这三个函数中,fold最为强大,不过也最为复杂。它的功能可以理解为:假定我们有三个值,初始值baseValue,函数fun,列表list,逐一访问list中的每个元素,对其应用函数fun,将fun的执行结果累加到baseValue,fold将baseValue的最终值返回。在逐一访问列表时,可以采用从左到右或从右向左的方式,所以fold函数有两个实现:fold_left和fold_right。
let accumulate acc x = acc + x
let totalList = List.fold_left accumulate 0 [1..100]
printfn "1+2+..+100 = %d" totalList // 5050
这里baseValue是0,函数是accumulate,列表是[1..100],最终结果为5050。
列表与模式匹配和递归的结合
初学列表时,容易像C#中的集合类型那样去看待它。最近学习了一下Haskell,为它的纯粹和优雅所折服,其中的列表部分大量使用了模式匹配和递归,这个过程也让我重新理解了列表。相比于F#的List模块,Haskell提供了额外的列表操作函数,这里我想通过在F#中实现这些函数来看看如何结合使用列表与模式匹配和递归。
take:接受两个参数,一个数字,一个列表,用于从列表开头获取指定个数的元素组成的新列表:
let rec take (count: int) (l: 'a list) =
match l with
| _ when count <= 0 -> []
| [] -> []
| x :: xs -> x :: take (count - 1) xs
let list1 = [1; 2; 3; 4; 5]
print_any(take 0 list1) // []
print_any(take 1 list1) // [1]
print_any(take 3 list1) // [1; 2; 3]
这里同时使用了递归和模式匹配,如果count小于等于0,返回空列表;否则返回从开头计数的指定个数的元素。
drop:该函数也接受两个参数,从列表开头移除指定个数的元素,将剩下的元素组成的列表返回:
let rec drop (count: int) (l: 'a list) =
match l with
| _ when count <= 0 -> l
| [] -> []
| head :: tail -> drop (count - 1) tail
let list1 = [1; 2; 3; 4; 5]
print_any(drop 0 list1) // [1; 2; 3; 4; 5]
print_any(drop 1 list1) // [2; 3; 4; 5]
print_any(drop 5 list1) // []
如果count小于等于0,返回原列表;否则移除指定个数的元素。这里使用了head和tail,这样代码的可读性会更好。
通过take和drop函数,我们可以看到,首先得把列表理解为链表,然后在此基础上应用递归和模式匹配,就可以完成很多复杂的操作。
小结
本文介绍了函数式编程(FP)中的列表操作。首先是函数式编程中列表的三种基本操作,在此基础上我们可以推导出其它的各种操作;随后介绍了F#中List模块中的重要函数;最后通过两个自定义函数来展示如何结合使用列表、递归和模式匹配。顺便提一句,强烈建议你学习一下Haskell来了解FP的基本思想,在F#中很容易就能使用命令式编程的方式编写程序,这种灵活性往往使人偏离FP,尤其是在初学FP时。这就像我们学习英语的过程,想象一下,如果把你空投到美国(或其它英语国家),你的英语的进步是不是会快得多?
前言
是的,我们已经学习了如何在F#中使用各种范式(函数式、命令式、面向对象)进行编程。但是目前还仅限于在单个模块内编写,要知道,不管是采用哪种语言或者范式编程,如果项目规模大了,都不适合把所有代码放在单个模块内。
在常规的.NET项目中(比如C#+ASP.NET),我们往往会选择使用Solution的概念作为整个(独立)问题域的解决方案,Solution以下则是Project、File。这些概念在物理上往往表现为程序集(类库或可执行程序)、类文件等,如果项目和文件数量较多,就该好好考虑如何在组织它们。下面从这三个层次上分别来讨论一下。
Solution层次
这里主要考虑的是Project之间的相互关系,此时基本上我们可以忽略语言的不同,也可以说在这个层次上,语言的影响不大。所以说我们把那些在用C#开发时采用的代码组织原则搬过来用。比如Martin Fowler在《企业应用架构模式》中谈到的内容,比如Robert Martin在《敏捷软件开发》中提到的关于包的设计原则,还包括.NET社区中关于PetShop架构的讨论等等,都可以加以借鉴。关于这方面的内容已有大量相关的讨论,在此不再赘述。
这里只谈一个具体的问题:如何添加对其它程序集的引用。在F# CTP 1.9.6.0之前,添加对程序集的引用需要#I和#r指令,#I用来指定要引用的程序集的目录,#r则用来指定要引用程序集的路径(包含文件名,可以是相对路径或绝对路径)。这两个指令既可以放在代码文件中,也可以放在编译选项中。其中有个小窍门,注册表中.NETFramework节点下包含了各.NET版本的一些信息,其中的AssemblyFoldersEx中有若干个目录信息,如果程序集所在目录出现在AssemblyFoldersEx中,就可以直接使用#r和文件名来添加引用了。
在CTP版本中,可以像常规的C#/VB.NET项目中那样,为项目添加对其它程序集的引用(包括引用同一解决方案中的其它项目):
而#r只能用于fsx脚本文件或者放在编译选项中。
Project层次
现在假定你已经对上述设计原则有了足够的了解,并运用这些原则完成了设计,下一步就是如何使用F#来实现这些设计。现在我们进入到了Project这个层次,需要考虑Project中各代码实体之间的关系,这些实体可以是物理上的源码文件,也可以是逻辑上的模块、类型、配置等。F#中最基本的组织结构是命名空间和模块,命名空间的概念与C#中的一样。借助于Reflector可以看到模块在编译之后就是静态类,我们在为模块添加成员时要了解,这是在向一个静态类添加成员。。
File层次
现在考虑源码文件内部的基本问题。在使用函数式编程范式时,除了模块,还可以采用F#的自定义类型,F#中的类型分为两类,一是元组(Tuple)或记录(Record)类型,它们类似于C#中的类;二是Union类型,有时又称为Sum类型。通过Reflector可以看到,元组值是Tuple类型的实例,而Tuple实现了 Microsoft.FSharp.Core.IStructuralHash和System.IComparable接口;记录和Union则直接实现了这两个接口。要了解IStructualHash接口的更多内容,请参考Jome Fisher的文章。
而在使用面向对象编程范式时,我们可以像在C#中那样定义.NET类型,比如接口、类、结构、枚举、委托等等。当然这其中的编程细节比较多,而且对于同一问题可以采取不同的方案。这需要我们去多多学习和实战,根据不同的需要作出选择。
这里来看另一个具体的问题:如何使用F#中的签名文件(Signature file)。在学习C语言时,接触过函数原型的概念,它给出了函数的名称、参数类型和返回类型,函数签名的含义与函数原型是一样的。如果我们把模块内的函数签名抽取出来,放在单独的一个文件中,这就是签名文件的由来。它的作用在于,它可以控制模块内函数的访问修饰符。如果要使用签名文件,那么它必须与其控制的模块文件成对出现,并且文件名相同。比如:
F# Code - myModule.fsi
#light
module FsLib.MyModule
/// 获取一个浮点数的平方值
val square: float -> float
/// 获取一个浮点数的立方值
val cube: float -> float
F# Code - myModule.fs
#light
module FsLib.MyModule
open System
let pow x y = Math.Pow(x, y)
let square x = pow x 2.0
let cube x = pow x 3.0
*.fsi即签名文件,这里定义了两个函数的签名:square和cube。*.fs即实现文件,它必须要提供对应的签名文件的所有函数的实现。其它程序集的模块,只能访问*.fsi中具有签名的函数。通过Reflector可以看到,对于myModule.fs中的三个函数,square和cube的修饰符为public,而pow则为internal。
由此看来签名文件的作用很像C#中的接口。但事实上,编译后并没有真正生成接口。需要注意的是,如果要为代码添加XML文档注释,需要加在签名文件(如果模块有的话)而不是模块中。下面来看看如何在代码中添加注释。
常规注释
在F#中,单行注释使用//,而多行注释则使用(* … *)。
XML文档注释
如果为代码添加了文档注释,可以在编译时生成XML文档,然后借助于一些工具(如SandCastle)就可以生成容易使用的帮助文档。在上面的代码中可以看到,直接使用///可以为模块或其成员添加文档注释,这个要比C#中简便一些。同时也完全可以使用C#中那样完整的文档注释格式(比如使用Summary、Param等节点)。
最后,如果要在F#使用C#类库中的代码,可以参考前面写过的了解关于这方面的内容。
F#的Project可以编译为类库或可执行应用程序(控制台应用程序或Windows应用程序)。我打算在后面的随笔就这两方面展开讨论,并尝试一些有实战意义的小型项目,相信到那时对代码组织的认识会更为准确。
小结
在初学F#时,我们可以很随便地将代码放在同一模块内做些尝试或者测试。但我们程序员不该是随便的人,随着项目规模的增大,代码的组织问题会变得越发重要,我们应当越加重视。在VS中进行开发时, 整个项目的组织自然地分为了Solution、Project、File三个层次,本文在这三个层次上就代码组织的基本问题做了讨论,写得比较简单,欢迎您来留言讨论 。
单元测试是开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确。通常情况下,一个单元测试(用例)用于判断某个特定条件(或场景)下特定函数的行为。如果想对单元测试的好处有更多的了解。
在F#中, LOP(Language-Oriented Programming)是它的一个亮点,而FsUnit则是LOP的一个很好的实践。FsUnit使用F#开发,用它编写的测试用例会接近于自然语言(英语),在其中我们也可以看到F#对函数进行组合的强大威力。
在本文中,我将通过简单的例子分别对NUnit和FsUnit的基本用法进行介绍。假定我们在开发一个类库MyFsLib,其中有一个模块mathHelper,里面有一些关于数学的函数,现在要做的就是测试这些函数。mathHelper的代码如下:
F# Code - mathHelper的签名
#light
module MyFsLib.MathHelper
/// 获取一个浮点数的平方值
val square: float -> float
/// 获取一个浮点数的立方值
val cube: float -> float
/// 判断一个整数是否为偶数
val isEven: int -> bool
/// 判断一个整数是否为奇数
val isOdd: int -> bool
/// 获取不大于指定正整数的质数数组
val generatePrimes: int -> int array
F# Code - mathHelper的实现
#light
module MyFsLib.MathHelper
open System
let pow x y = Math.Pow(x, y)
let square x = pow x 2.0
let cube x = pow x 3.0
let isEven x = x % 2 = 0
let isOdd x = x % 2 = 1
// Eratosthenes筛法
let generatePrimes n =
match n with
| _ when n < 2 -> [||]
| _ ->
// Init sieve.
let sieve = [| for i in 0 .. n do yield true |]
let isPrime index = sieve.[index]
// Check it.
let upperBound = Convert.ToInt32(Math.Sqrt((float)n))
for i = 2 to upperBound do
if isPrime i then
for j in [i * 2 .. i .. sieve.Length - 1] do
sieve.[j] <- false
let mutable count = 0
for i = 2 to sieve.Length - 1 do
if isPrime i then
count <- count + 1
let primes = Array.create count 0
let mutable index = 0
for i = 2 to sieve.Length - 1 do
if isPrime i then
primes.[index] <- i
index <- index + 1
primes
使用NUnit进行单元测试
不要害怕,由于F#植根于.NET平台的本性,你会发现这些测试用例代码都是那么眼熟。
需要废话的是,先添加对”nunit.framework.dll”的引用,而且要为测试类添加一个无参构造函数。
F# Code - NUnit tester
#light
namespace NUnitTester
open NUnit.Framework
open MyFsLib
[<TestFixture>]
type TestCases = class
new() = {}
[<Test>]
member this.TestSquare() =
Assert.AreEqual(0, MathHelper.square(0.0))
Assert.AreEqual(4, MathHelper.square(2.0))
[<Test>]
member this.TestGeneratePrimes() =
let primesLessThan2 = MathHelper.generatePrimes(1)
CollectionAssert.IsEmpty(primesLessThan2)
let primesNotGreaterThan2 = MathHelper.generatePrimes(2)
CollectionAssert.IsNotEmpty(primesNotGreaterThan2)
CollectionAssert.Contains(primesNotGreaterThan2, 2)
Assert.AreEqual(1, primesNotGreaterThan2.Length)
let primesNotGreaterThan10 = MathHelper.generatePrimes(10)
CollectionAssert.IsNotEmpty(primesNotGreaterThan10)
CollectionAssert.Contains(primesNotGreaterThan10, 7)
Assert.AreEqual(4, primesNotGreaterThan10.Length)
// Other testcases
end
这里只编写了对函数square和generatePrimes的测试。如果你在C#中用过NUnit,看这样的代码就没有任何问题了。测试结果为:
使用FsUnit进行单元测试
FsUnit是一个Specification测试框架。它的目标是将单元测试和行为(函数)的规格尽量简化,并以函数式的风格代替命令式风格的测试代码。
Specification可以翻译为规格说明,就是说测试代码实际上是对待测代码的一条条规格说明。比如对函数square,它求一个数的平方,那么一条规格可以是:”square(2) should equal 4”。好了,惊喜就要来了:
F# Code - FsUnit tester
#light
open FsUnit
open MyFsLib
let squareSpecs =
specs "Test square" [
spec "square(0) should equal 0"
(MathHelper.square(0.0) |> should equal 0.0) // Pass
spec "square(2) should equal 2"
(MathHelper.square(2.0) |> should equal 2.0) // Fail
]
let generatePrimes1Specs =
specs "Test generatePrimes" [
spec "generatePrimes(1).Length should equal 0"
(MathHelper.generatePrimes(1).Length |> should equal 0)
]
let generatePrimes2Specs =
specs "Test generatePrimes" [
spec "generatePrimes(2).Length should equal 1"
(MathHelper.generatePrimes(2).Length |> should equal 1)
spec "generatePrimes(2) should contain 2"
(MathHelper.generatePrimes(2) |> should contain 2)
]
let generatePrimes10Specs =
specs "Test generatePrimes" [
spec "generatePrimes(10).Length should equal 4"
(MathHelper.generatePrimes(10).Length |> should equal 4)
spec "generatePrimes(10) should contain 7"
(MathHelper.generatePrimes(10) |> should contain 7)
spec "generatePrimes(10) should not contain 9"
(MathHelper.generatePrimes(10) |> should not' (contain 9))
]
printfn "%s" (Results.summary())
这里没有Assert,有的是should,这两个词给人的感觉可大不一样,而且通过函数的组合,我们可以写出should equal这样的“句子”。这里的测试代码已经比较接近自然语言了。
一条spec就是一条规格说明,它说明待测的函数具有什么样的规格,我们把这些都放在specs中,测试结束后,使用Results.summary函数来显示测试结果:
如果对FsUnit感兴趣,可以到http://code.google.com/p/fsunit/这里来看看。感觉目前它还有些欠缺,比如没有像CollectionAssert这样的测试类,接下来看看能不能扩展一下。
小结
本文介绍了在F#中如何使用NUnit和FsUnit进行单元测试。可以看到两者都很简单,前者简单是因为能很好地延续在C#中的方式,迁移过来不要费多大力气;后者简单是因为它接近自然语言,看起来很亲切。FsUnit值得关注,除了单元测试本身,我们还可以通过它来了解Language-Oriented Programming的相关知识。
模式匹配(Pattern Matching)允许我们根据标识符值的不同进行不同的运算,它通常被拿来跟C#中的if…else或switch语法结构相比较,结论往往是模式匹配比后者要更为灵活、强大。那先来分析一下它灵活、强大在哪儿。
为什么说模式匹配是灵活、强大的?
在我前面写过的几篇随笔里面,有几次提到了模式匹配,比如它能够对简单值(整数、字符串)匹配,也可以对.NET类型进行匹配,看下面两个简单的例子:
F# Code - 对简单值和.NET类型进行匹配
// 对简单值进行匹配。
let rec fibonacci x =
match x with
| x when x <= 0 -> failwith "x必须是正整数。"
| 1 -> 1
| 2 -> 1
| x -> fibonacci(x - 1) + fibonacci(x - 2)
printfn "%i" (fibonacci 2) // -> 1
printfn "%i" (fibonacci 4) // -> 3
// 对.NET类型进行匹配。
open System
let typeToString x =
match box x with
| :? Int32 -> "Int32"
| :? Double -> "Double"
| :? String -> "String"
| _ -> "Other Type"
可以看到,这里所用的模式匹配没有给人太多惊喜,不用费多大力气就可以将其转换为if…else或switch结构了。
先别急着离开,列表是FP中的典型数据结构,我们对它应用一下模式匹配看看。
F# Code - 对列表应用模式匹配
// 对列表应用模式匹配。
let listOfList = [[2; 3; 5]; [7; 11; 13]; [17; 19; 23; 29]]
let rec concatenateList list =
match list with
| head :: tail -> head @ (concatenateList tail)
| [] -> []
let rec concatenateList2 list =
if List.nonempty list then
let head = List.hd list in
let tail = List.tl list in
head @ (concatenateList2 tail)
else
[]
let primes = concatenateList listOfList
print_any primes // [2; 3; 5; 7; 11; 13; 17; 19; 23; 29]
listOfList是一个列表的列表,两个函数concatenateList和concatenateList2的功能都是将listOfList的元素连接为一个大的列表,只不过一个用模式匹配方式实现,一个使用if…then…else结构实现。可以看到concatenateList的代码更为简洁,但仅仅如此吗?在concatenateList2中,我们按照传统的看待链表(F#中的列表以链表实现)的方式,将其中的节点一个一个取出来进行处理,这种处理方式是较为具体和细节的;而在concatenateList中我们通过两个简单的模式“head :: tail”和“[]”就覆盖了列表的所有可能,可以说,我们找到了更好地分解列表这种数据结构的方式,从而可以更为通用地处理列表。
类似的,再来看看Union类型的情况。Union类型,有时称为sum类型或discriminated union,可将一组具有不同含义或结构的数据组合在一起。它的一个典型应用是表示一颗树:
F# Code - 对Union类型应用模式匹配
type BinaryTree<'a> =
| Leaf of 'a
| Node of BinaryTree<'a> * BinaryTree<'a>
let rec printBinaryTreeValues t =
match t with
| Leaf x -> printfn "%i" x
| Node (l, r) ->
printBinaryTreeValues l
printBinaryTreeValues r
printBinaryTreeValues (Node ((Node (Leaf 1, Leaf 2)), (Node (Leaf 3, Leaf 4))))
这里通过BinaryTree<'a>定义一个泛型二叉树类型,printBinaryTreeValues函数用于打印其节点的值,这里需要判断节点的类型(子树还是叶子),有趣的是,Leaf和Node自动抽象为“模式”,不需要任何额外的工作。这样就可以看到一些所谓“灵活、强大”的影子了,对于Union类型所表示的数据结构,模式匹配可以极为简单、自然地分解、处理它。
除了列表和Union类型,元组对于模式匹配的“自适应”也是类似的,这些已经够我们解决很多问题了。那对于其它的更复杂的场景或者更特殊的领域,F#还有什么大招呢?你一定能想得到,这就是活动模式。
活动模式(Active Pattern)
活动模式的思想就是把模式匹配语法用于其他更多的数据结构。可以把它分为Single-Case、Multi-Case、Partial这几种类型。我将逐一做出介绍。
Single-Case活动模式
Single-Case是最简单的活动模式形式,它将一个输入值转换为其它的值,比如:
F# Code - Single-Case活动模式
let (|UpperCase|) (x:string) = x.ToUpper()
let result = match "foo" with
| UpperCase "FOO" -> true
| _ -> false
printfn "%b" result // -> true
这里的UpperCase就是一个模式,它的类型信息为:active recognizer UpperCase: string -> string,可以看到下面求result值的时候可以像前面一样使用模式匹配的语法了,UpperCase “FOO”可以理解为对于输入值”foo”,应用了UpperCase模式后,结果应当为”FOO”,如果确实如此,那么该模式就匹配了,所以result的值为true。
UpperCase模式看起来像是一个函数,不过对于函数来说,没法直接应用模式匹配的语法。
Multi-Case活动模式
F# Code - Multi-Case活动模式
let (|Odd|Even|) x = if x % 2 = 0 then Even else Odd
let isDivisibleByTwo x = match x with Even -> true | Odd -> false
print_any (isDivisibleByTwo 2) // -> true
print_any (isDivisibleByTwo 3) // -> false
这里(|Odd|Even|)就是Multi-Case模式了,Even的类型信息为:active recognizer Even: int -> unit,即它没有返回值,所以在匹配时,直接写Even或Odd就可以了。
Partial活动模式
简单来说,Partial模式就是那些并不总是返回值的模式。比如输入值的范围可能过于庞大,或者对于某些返回值我们并不感兴趣,可以将其忽略。比如,对于自然数来说,只有一小部分是完全平方数或者能够被7整除。
F# Code - Partial活动模式
// Partial Active Patterns
open System
let (|DivisibleBySeven|_|) input = if input % 7 = 0 then Some() else None
let (|IsPerfectSquare|_|) (input : int) =
let sqrt = int (Math.Sqrt(float input))
if sqrt * sqrt = input then
Some()
else
None
let describeNumber x =
match x with
| DivisibleBySeven & IsPerfectSquare ->
printfn "x is divisible by 7 and is a perfect square."
| DivisibleBySeven -> printfn "x is divisible by seven."
| IsPerfectSquare -> printfn "x is a perfect square."
| _ -> printfn "x looks normal."
describeNumber 49 // x is divisible by 7 and is a perfect square.
describeNumber 35 // x is divisible by seven.
describeNumber 25 // x is a perfect square.
describeNumber 20 // x looks normal.
自然数有很多特性,而在函数describeNumber中,我们只关注它是否是完全平方数或者7的倍数,其它的就都舍弃不管了。
应用
我们来看看如何使用活动模式来操作XML文档。
F# Code - 应用活动模式操作XML文档
// 定义针对XML节点的模式
let (|Node|Leaf|) (node : #System.Xml.XmlNode) =
if node.HasChildNodes then
Node (node.Name, { for x in node.ChildNodes -> x })
else
Leaf (node.InnerText)
// 打印XML节点的函数
let printXml node =
let rec printXml indent node =
match node with
| Leaf (text) -> printfn "%s%s" indent text
| Node (name, nodes) ->
printfn "%s%s:" indent name
nodes |> Seq.iter (printXml (indent + " "))
printXml "" node
// 定义XML节点
let doc =
let temp = new System.Xml.XmlDocument()
let text = "<fruit>
<apples>
<gannySmiths>1</gannySmiths>
<coxsOrangePippin>3</coxsOrangePippin>
</apples>
<organges>2</organges>
<bananas>4</bananas>
</fruit>"
temp.LoadXml(text)
temp
printXml (doc.DocumentElement :> System.Xml.XmlNode)
这里首先定义针对XML节点的模式,然后应用该模式来递归打印出一个XML节点及其子节点的信息。
可以看到使用活动模式,寥寥数语就可以描述出XML节点的通用数据结构来了,这为接下来对节点的操作提供了良好的基础,而且我们回归了问题本身——XML文档,而不需要关注具体的编程细节。
小结
这里先是介绍了F#中模式匹配的用法,这个可以理解为使用F#内置的模式,这样我们就可以处理F#中的值和特定的数据结构,比如列表、Union类型和元组等;接下来更进一步,活动模式把模式匹配的语法用到了其他更多的数据结构,这样模式的应用范围得到了很大的扩展。而且通过活动模式,我们可以将问题域转换为一套术语来表达,从而脱离编程细节回归到问题域本身,这也就有了一些LOP(Language-Oriented Programming)的特点,事实上,活动模式正是F#中LOP的实现方式之一。这个我将在后面的随笔做更深入的讨论。
引言
在《Foundations of F#》的第七章中,作者在介绍Math命名空间时举的例子是绘制Mandelbrot集合。这个看起来挺奇怪的东东以前还真没见过,网上一查才知道,原来它是如此的优美动人。由于该集合的定义与分形相关,所以先来了解下分形的概念。
什么是分形(Fractal)
1967年,美国数学家Mandelbrot曾出这样一个著名的问题:英格兰的海岸线到底有多长?这个问题在数学上可以理解为:用折线段拟合任意不规则的连续曲线是否一定有效?这个问题的提出实际上是对以欧氏几何为核心的传统几何的挑战。
1975年,Mandelbrot在其《自然界中的分形几何》一书中引入了分形(fractal)这一概念。从字面意义上讲, fractal是碎块、碎片的意思,然而这并不能概括Mandelbrot的分形概念,尽管目前还没有一个让各方都满意的分形定义,但在数学上大家都认为分形有以下几个特点:
1. 具有无限精细的结构;
2. 比例自相似性;
3. 一般它的分数维大子它的拓扑维数;
4. 可以由非常简单的方法定义,并由递归、迭代产生。
据说,南非海岸线的维数是1.02,英国西岸的维数是1.25。
下面的两幅图有助于我们理解它的概念:
需要注意的是,分形往往由递归、迭代产生,但是我们在纸上做出的图只能作有限次的递归、迭代。
分形几何学已在自然界与物理学中得到了应用。如在显微镜下观察落入溶液中的一粒花粉,会看见它不间断地作无规则运动(布朗运动),这是花粉在大量液体分子的无规则碰撞(每秒钟多达十亿亿次)下表现的平均行为。布朗粒子的轨迹,由各种尺寸的折线连成。只要有足够的分辨率,就可以发现原以为是直线段的部分,其实由大量更小尺度的折线连成。
什么是Mandelbrot集合?
Mandelbrot集合是在复平面上组成分形的点的集合,它正是以数学家Mandelbrot命名。
Mandelbrot集合可以用复二次多项式
其中c是一个复参数。对于每一个c,从 开始对fc(z)进行迭代。
序列 的元素的模(复数具有模的概念)或者延伸到无穷大,或者只停留在有限半径的圆盘内。Mandelbrot集合就是使以上序列不延伸至无限大的所有c点的集合。
从数学上来讲,Mandelbrot集合是一个复数的集合。一个给定的复数c或者属于Mandelbrot集合M,或者不属于。比如,取c = 1,那么这个序列就是(0, 1, 2, 5, 26, ...),显然它的值会趋于无穷大;而如果取c = i,那么序列就是(0, i, -1+i, -i, -1+i, -i,...),它的值会一直停留在有限半径的圆盘内。
事实上,一个点属于Mandelbrot集合当且仅当它对应的序列(由上面的二项式定义)中的任何元素的模都不大于2。这里的2就是上面提到的“有限半径”。
在计算机上绘制Mandelbrot集合
计算机的屏幕上的像素只有有限个,而Mandelbrot集合中的点则有无限个。
观察上面复平面的局部,Mandelbrot集合即黑色区域,实部从-2到1,虚部从-1到1,那么将两个点(-2, 1)和(1, -1)作为一个矩形的左上角顶点和右下角顶点,那么这个矩形就包含了整个Mandelbrot集合,该矩形的长为3,宽为2。我们可以将这个矩形与屏幕上的区域进行映射,也就是将屏幕上的一个像素映射为该矩形内的一点,如果该点属于Mandelbrot集合,就将该像素着为黑色,这样逐一对每个像素进行判断和着色,就可以模拟绘制Mandelbrot集合了。该矩形的长宽比为3:2,我们在屏幕上可以取600 * 400的矩形区域。
完成映射后来考虑如何判断一个点是否属于该集合。其根据就是上面的结论“一个点属于Mandelbrot集合当且仅当它对应的序列(由上面的二项式定义)中的任何元素的模都不大于2”,由于序列的的元素有无穷多个,我们只能取有限的迭代次数来模拟了,比如取100或1000次。
我们用Microsoft.FSharp.Math.Notation.complex类型来表示一个复数,它的Magnitude属性表示复数的模,我们可以通过一定次数(比如100次)的迭代,来看看前100项是不是都满足条件,如果满足就认为这个复数在Mandelbrot集合内。下面是完整的程序。
F# Code - 绘制Mandelbrot集合
#light
open System
open System.Drawing
open System.Windows.Forms
open Microsoft.FSharp.Math
open Microsoft.FSharp.Math.Notation
// 迭代次数
let maxIterations = 100
// 映射比例
let scalingFactor = 1.0 / 200.0
// 将像素映射为复数
let mapPlane(x, y) =
let fx = ((float x) * scalingFactor) - 2.0
let fy = ((float y) * scalingFactor) - 1.0
complex fx fy
let mutable iteration = 0
let mutable current = complex 0.0 0.0
let mutable temp = complex 0.0 0.0
let form =
let image = new Bitmap(600, 400)
for x = 0 to image.Width - 1 do
for y = 0 to image.Height - 1 do
iteration <- 0
current <- mapPlane(x, y)
temp <- current
// 判断当前点是否在Mandelbrot集合内
while(temp.Magnitude <= 2.0 && iteration < maxIterations) do
temp <- temp * temp + current
iteration <- iteration + 1
// 如果在,像素为黑色
if iteration = maxIterations then
image.SetPixel(x, y, Color.Black)
else
image.SetPixel(x, y, Color.White)
let temp = new Form() in
temp.Paint.Add(fun e -> e.Graphics.DrawImage(image, 0, 0))
temp.Height <- 435
temp.Width <- 600
temp.Text <- "Draw Mandelbrot Set"
temp
[<STAThread>]
do Application.Run(form)
下面是效果图
这张图是黑白的,我们可以把它变成彩色的。看这一部分代码:
// 如果在,像素为黑色
if iteration = maxIterations then
image.SetPixel(x, y, Color.Black)
else
image.SetPixel(x, y, Color.White)
只有iteration等于maxIterations,当前的复数才属于Mandelbrot集合,这时将像素着为黑色;如果不在集合内,我们可以想办法着为彩色。考虑红橙黄绿蓝靛紫七种颜色,把它们存储在数组中,然后根据iteration的值来取相应的颜色:
Code
// 七种颜色
let colors = [| Color.Red; Color.Orange; Color.Yellow;
Color.Green; Color.Blue; Color.Indigo;
Color.Purple; |]
// 如果在,像素为黑色
if iteration = maxIterations then
image.SetPixel(x, y, Color.Black)
else
image.SetPixel(x, y, colors.[iteration % colors.Length])
完整的代码是:
F# Code - 彩色的Mandelbrot
#light
open System
open System.Drawing
open System.Windows.Forms
open Microsoft.FSharp.Math
open Microsoft.FSharp.Math.Notation
// 迭代次数
let maxIterations = 30
// 七种颜色
let colors = [| Color.Red; Color.Orange; Color.Yellow;
Color.Green; Color.Blue; Color.Indigo;
Color.Purple; |]
// 映射比例
let scalingFactor = 1.0 / 200.0
// 将像素映射为坐标
let mapPlane(x, y) =
let fx = ((float x) * scalingFactor) - 2.0
let fy = ((float y) * scalingFactor) - 1.0
complex fx fy
let mutable iteration = 0
let mutable current = complex 0.0 0.0
let mutable temp = complex 0.0 0.0
let form =
let image = new Bitmap(600, 400)
for x = 0 to image.Width - 1 do
for y = 0 to image.Height - 1 do
iteration <- 0
current <- mapPlane(x, y)
temp <- current
// 判断当前点是否在Mandelbrot集合内
while(temp.Magnitude <= 2.0 && iteration < maxIterations) do
temp <- temp * temp + current
iteration <- iteration + 1
// 如果在,像素为黑色
if iteration = maxIterations then
image.SetPixel(x, y, Color.Black)
else
image.SetPixel(x, y, colors.[iteration % colors.Length])
let temp = new Form() in
temp.Paint.Add(fun e -> e.Graphics.DrawImage(image, 0, 0))
temp.Height <- 435
temp.Width <- 600
temp.Text <- "Drawing Mandelbrot Set"
temp
[<STAThread>]
do Application.Run(form)
下面是效果图
当然这只是着彩色方式的一种,如果你有兴趣,可以查看后面给出的参考文章。
小结
Mandelbrot集合的图形表示可以让我们认识到纯粹的数学之美,与之相关的分形几何学则是无处不在的,不得不感叹数学的力量。由于分形几何学知识的匮乏,本文只能给出Mandelbrot集合的定义,并以最容易理解的方式绘制出该集合。这里使用的语言是F#,而不是C#,以后会尽量作出一些采用F#的更为实用的例子。
什么是Eratosthenes筛法
考虑一个常见的数论问题,指定一个整数,求出不大于该数的所有质数。我们可以先写一个函数来判断某个整数是否为质数,然后用它逐一判断每个整数,而Eratosthenes筛法比这种方法高效得多。
下面举例来说明它的原理。观察下面的彩图(来自wikipedia),这里是检查120以内的所有质数。首先去掉1,因为1既不是质数也不是合数;然后2是质数,那么2的倍数都肯定不是质数了,所以把2的倍数都涂成红色(它们出局了,这里是质数的游戏...);接下来3没有被涂掉,所以3是质数,同理把3的倍数涂成绿色;下一个未涂掉的是5,所以5也是质数,同理把5的倍数涂掉;然后再涂掉7的倍数,剩下的就都是质数了。
怎么,只要看2、3、5、7就够了?这里有两个不那么容易确定的问题,一是如何依次断定3、5、7都是质数;二是为何涂掉7的倍数后就完成了。对于问题一,如果一个整数不能被比它小的所有质数整除(这里就是没被涂掉),那么我们就可以断定它是质数。另外,该筛法的依据是,对于整数n,如果它不能被不大于n的平方根(Sqrt(n))的任何质数所整除,那么n就是质数。这样就解决了问题二,对于120以内的整数,如果它不是质数,那么它必定能被不大于10的质数(也就是2、3、5、7)所整除。
这样我们可以确信上述算法是正确的了,下面来看看如何实现它。
Eratosthenes筛法的C#和F#实现
C# Code - Eratosthenes筛法
// 返回不大于n的质数构成的数组
public static int[] SieveOfEratosthenes(int n)
{
if (n < 2) { return new int[0]; }
// Init sieve.
bool[] sieve = new bool[n + 1];
for (int i = 2; i < sieve.Length; i++) { sieve[i] = true; }
// Check it.
int upperBound = Convert.ToInt32(Math.Sqrt(n));
for (int i = 2; i <= Math.Sqrt(n); i++)
{
if (sieve[i])
{
for (int j = i + i; j <= n; j += i)
{
sieve[j] = false;
}
}
}
// Count it.
int count = 0;
for (int i = 2; i < sieve.Length; i++)
{
if (sieve[i])
{
count++;
}
}
// Generate the result.
int[] primes = new int[count];
int index = 0;
for (int i = 2; i < sieve.Length; i++)
{
if (sieve[i])
{
primes[index++] = i;
}
}
return primes;
}
下面是F#实现,其中getInt和main是辅助函数:
F# Code - Eratosthenes筛法
#light
open System
let generatePrimes n =
match n with
| _ when n < 2 -> [||]
| _ ->
// Init sieve.
let sieve = [| for i in 0 .. n do yield true |]
let isPrime index = sieve.[index]
// Check it.
let upperBound = Convert.ToInt32(Math.Sqrt((float)n))
for i = 2 to upperBound do
if isPrime i then
for j in [i * 2 .. i .. sieve.Length - 1] do
sieve.[j] <- false
let mutable count = 0
for i = 2 to sieve.Length - 1 do
if isPrime i then
count <- count + 1
let primes = Array.create count 0
let mutable index = 0
for i = 2 to sieve.Length - 1 do
if isPrime i then
primes.[index] <- i
index <- index + 1
primes
let getInt() =
Convert.ToInt32(Console.ReadLine())
let main() =
let i = getInt()
let primes = generatePrimes i
Console.WriteLine("find {0} prime(s)", primes.Length)
print_any primes
main()
Console.Read()
这两段代码也就勉强能实现算法吧,有时间再考虑优化一下。
“失足”的NASA气象卫星
1998年2月,美国宇航局(NASA)发射了一枚探测火星气象的卫星,预定于1999年9月23日抵达火星。然而研究人员惊讶地发现,卫星没有进入预定的轨道,却陷入了火星大气层,很快就烟消云散了。NASA的官员经过紧急调查,发现问题居然出在有些资料的计量单位没有把英制(English)转换成公制(Metric),错误起自承包工程的洛克希德马丁航天公司。美国企业包括太空工业使用英制,喷射推进实验室(国家实验室)使用公制,承包商理应把英制都转换成公制,以便喷射推进实验室每天两次启动小推进器,来调整太空船的航向。导航员认定启动小推进器的力是以公制的"牛顿"为单位。不料,洛克希德马丁公司提供的资料却是以英制的"磅"为单位,结果导致太空船的航向出现微小偏差。日积月累,终于差之毫厘,失之千里。
这个英制未换算成公制的"小错误"造成的损失有多大呢?其他损失不计,单单卫星的造价就高达1.25亿美元,这些费用就这样全泡了汤。理论上,如果使用统一的度量衡计量单位制,这样的损失本是可以避免的。
何为计量单位
早上醒来,看到天气预报今天温度是4-12 ℃,不算太冷;离班车发车只有20分钟了,得抓紧点;午饭几个同事一起出去吃,每个人10块钱;晚上回来下车后,去水果摊买了几个苹果,是10块钱3斤的那种;回去一看电费单又来了,用了60度电……,我们每天都得跟“单位” 打交道。
所谓单位(Unit)是指给定的某一基础量,它通常伴随着某种表示方法,例如米、秒、公斤等,以方便人们在沟通某一量时有共通的概念。而计量单位(Unit of Measurement)是单位的具体统称,为人们计算一个数额的方法。如长度单位可以小至纳米、微米、毫米,也可以大到千米、光年等;时间单位可以是微秒、秒、分钟到日、周、月、年等等。
基本单位与导出单位
各种物理量都有它们的量度单位,并以选定的物质在规定条件下所显示的数量作为基本量度单位的标准,在不同时期和不同的学科中,基本量的选择可以不同。如物理学上以时间、长度、质量、温度、电流强度、发光强度、物质的量这7个物理单位为基本量,它们的单位依次为:秒、米 (单位)、千克、开尔文、安培、坎德拉、摩尔(可以翻一下高中物理课本,呵呵)。
由基本量根据有关公式推导出来的其他量,叫做导出量。导出量的单位叫做导出单位。如速度的单位是由长度和时间单位组成的用“m/s”表示。
单位的换算
不同的国家或文化中,单位制可能不同,如我们常用的“尺”(1米=3尺)、英尺(英制单位)、米(公制单位)等等。现在NBA中球员的身高一般仍使用英制单位表示,如麦迪的身高是6英尺8英寸,也就是2米03。你可以说一个人的三围是34、24、34(英寸),也可以说是86、61、86(厘米)。两个人进行交流时,显然使用统一的计量单位会更好些,NASA气象卫星就是一个好例子。
F#对计量单位的支持
显然NASA使用的编程语言不支持计量单位,于是很多人希望扩展语言以添加对计量单位的静态检查。F#的CTP(2008年9月)版本提供了单位的静态检查和推导功能。这些功能有趣而且实用。
先来看一下如何定义一个单位。
F# Code - 定义计量单位
[<Measure>]
type kg
[<Measure>]
type s
[<Measure>]
type m
注意Measure特性,这里通过它定义了三个单位,分别是千克、秒和米。虽然这里使用了type关键字,但是这里仅仅用于创建计量单位,通过Reflector可以看到,根本没有kg、s和m的踪影。来看看如何使用这些单位。
F# Code - 使用计量单位
let heightOfMyOfficeWindow = 3.5<m> // -> float<m>
let gravityOnEarch = 9.81<m/s^2> // -> float<m/s^2>
这里定义了两个量,一个表示高度,单位为米,一个表示重力加速度,单位为米/秒2。(这里的m/s^2也可写作m s^-2或m/s/s,从数学意义上来看都是等价的)好,现在考虑下,如果从窗户跳下去,做自由落体运动,落地时速度会有多少:
let speedOfImpact = sqrt (2.0 * gravityOnEarch * heightOfMyOfficeWindow)
// speedOfImpact: float<m/s>
print_any speedOfImpact // 8.28673639 m/s
计算结果为约8.3m/s,貌似很疼吧!这里最神奇的地方在于,F#竟然知道speedOfImpact的单位是m/s,太酷了!看计算公式 sqrt (m/s^2 * m),这样更容易理解些。如果将两个单位不同的量相加,F#会报告错误:
再来计算一下我受到的重力是多少:
F# Code - 计算重力
let myMass = 75.0<kg>
let forceOnGround = myMass * gravityOnEarch // -> kg m/s^2
forceOnGround的单位是 kg m/s^2,即物理学中力的单位:牛顿。它是导出单位的一个例子,我们可以为它建立一个新的单位:
F# Code - 导出单位
[<Measure>]
type N = kg m/s^2
let forceOnGround: float<N> = myMass * gravityOnEarch // -> N
导出单位的作用就像类型的别名一样,所以在F#中,N和kg m/s^2是一样的。
F#与国际单位制
在F#的FSharp.PowerPack.dll中包含了国际单位制中的基本单位和导出单位,其模块是Microsoft.FSharp.Math.SI(SI即国际单位制的缩写),也包含了物理常量,在PhysicalConstants模块中。来看看如何计算万有引力:
F# Code - 计算万有引力
open Microsoft.FSharp.Math
open SI
let gravitationalForce
(d: float<m>)
(m1: float<kg>)
(m2: float<kg>) : float<N> =
m1 * m2 / (d * d) * PhysicalConstants.G
这里的m、kg和N都定义在SI中,G是引力常量。
F#中的单位换算
如果每个人都用标准单位就好了。但F#也允许你使用其它单位,还是考虑那个自由落体运动,这次单位是英尺(feet)。
F# Code - 使用非公制单位
[<Measure>]
type ft
let gravityOnEarch = 32.2<ft/s^2>
let heightOfMyOfficeWindow = 11.5<ft>
let speedOfImpact = sqrt (2.0 * gravityOnEarch * heightOfMyOfficeWindow)
此时速度speedOfImpact的单位就是ft/s了。如果需要在英尺和米之间进行转换的话,可以先定义一个转换因子,然后在进行转换:
F# Code - 单位换算
let feetPerMetre = 3.28084<ft/m>
let heightOfMyOfficeWindowInMetres =
heightOfMyOfficeWindow / feetPerMetre
事实上,F#认为ft和m没有任何关系,所以转换因子有我们开发人员来定。可以认为转换因子就是定义了一个常量,这样出错的几率会减少很多。另一种方式是将转换因子封装在计量单位中,如:
F# Code - 封装转换因子
[<Measure>]
type ft =
static member perMetre = 3.28084<ft/m>
需要注意的是,此时ft已经成为真正的类型,perMetre是它的一个静态属性。
另外,如何将一个单纯的数字转换带有单位的量呢?可以给它乘上一个单位量,如:
F# Code - 使用单位量
let rawString = Console.ReadLine()
let rawFloat = Double.Parse(rawString)
let timeInSeconds = rawFloat * 1.0<s>
timeInSeconds的类型是float<s>,如果要将它重新转换为纯数字,可以让它除以1.0<s>:
let timeSpan = TimeSpan.FromSeconds(timeInSeconds / 1.0<s>)
注意,timeInSeconds / 1.0<s>的类型为float<1>,这种数字称为无因次量,即没有单位的量。
计量单位也泛型
计量单位也能与泛型扯上关系?考虑下面的函数:
let sqr x = x * x
sqr的类型为int -> int,F#认为它的参数和返回类型都是int,应用类型标注,可以给参数指定类型:
let sqrMass (x: float<kg>) = x * x
let sqrLength (x: float<m>) = x * x
let sqrSpeed (x: float<m/s>) = x * x
闻到坏味了吗?难道要为每个单位都写一个函数?泛型的需求就出来了。可以这么来写:
F# Code - 泛型计量单位
let sqr (x: float<_>) = x * x
sqr的类型为float<’u> -> float<’u^2>,’u表示sqr接受任何单位的值,包括float类型本身。如果我们要求出一个列表所有元素的和,我们可以使用List模块的fold函数,此时也可以使用泛型单位:
F# Code - 泛型单位值
let sum xs = List.fold_left (+) 0.0<_> xs
sum的类型为float<’u> list -> float<’u>。
应用
为了纪念一下消失在火星大气层的NASA气象卫星,我们这里使用F#模拟一下太阳系(Solar System),这个例子来自MSDN的F# Samples。
F# Code - 模拟太阳系
//----------------------------------------------------------------------------
// A Simple Solar SYstem Simulator, using Units of Measure
//
// Copyright (c) Microsoft Corporation 2005-2008.
//
// This sample code is provided "as is" without warranty of any kind.
// We disclaim all warranties, either express or implied, including the
// warranties of merchantability and fitness for a particular purpose.
//----------------------------------------------------------------------------
#light
open System
open System.Windows.Forms
open System.Drawing
open Microsoft.FSharp.Control.CommonExtensions
//-----------------------------------------------
// Graphics System
// Define a special type of form that doesn't flicker
type SmoothForm() as x =
inherit Form()
do x.DoubleBuffered <- true
let form = new SmoothForm(Text="F# Solar System Simulator", Visible=true, TopMost=true, Width=500, Height=500)
type IPaintObject =
abstract Paint : Graphics -> unit
// Keep a list of objects to draw
let paintObjects = new ResizeArray<IPaintObject>()
form.Paint.Add (fun args ->
let g = args.Graphics
// Clear the form
g.Clear(color=Color.Blue)
// Draw the paint objects
for paintObject in paintObjects do
paintObject.Paint(g)
// Invalidate the form again in 10 milliseconds to get continuous update
async { do! System.Threading.Thread.AsyncSleep(10)
form.Invalidate() } |> Async.Spawn
)
// Set things going with an initial Invaldiate
form.Invalidate()
//-----------------------------------------------
[<Measure>]
type m
[<Measure>]
type km
[<Measure>]
type AU
[<Measure>]
type sRealTime
[<Measure>]
type s
[<Measure>]
type kg
[<Measure>]
type pixels
type System.TimeSpan with
member x.TotalSecondsTyped = (box x.TotalSeconds :?> float<sRealTime>)
let G = 6.67e-11<m ^ 3 / (kg s^2)>
let m_per_AU = 149597870691.0<m/AU>
let AU_per_m = 1.0/m_per_AU
let Pixels_per_AU = 200.0<pixels/AU>
let m_per_km = 1000.0<m/km>
let AU_per_km = m_per_km * AU_per_m
// Make 5 seconds into one year
let sec_per_year = 60.0<s> * 60.0 * 24.0 * 365.0
// One second of real time is 1/40th of a year of model time
let realTimeToModelTime (x:float<sRealTime>) = float x * sec_per_year / 80.0
let pixels (x:float<pixels>) = int32 x
type Planet(ipx:float<AU>,ipy:float<AU>,
ivx:float<AU/s>,ivy:float<AU/s>,
brush:Brush,mass:float<kg>,
width,height) =
// For this sample e store the simulation state directly in the object
let mutable px = ipx
let mutable py = ipy
let mutable vx = ivx
let mutable vy = ivy
member p.Mass = mass
member p.X with get() = px and set(v) = (px <- v)
member p.Y with get() = py and set(v) = (py <- v)
member p.VX with get() = vx and set(v) = (vx <- v)
member p.VY with get() = vy and set(v) = (vy <- v)
interface IPaintObject with
member obj.Paint(g) =
let rect = Rectangle(x=pixels (px * Pixels_per_AU)-width/2,
y=pixels (py * Pixels_per_AU)-height/2,
width=width,height=height)
g.FillEllipse(brush,rect)
type Simulator() =
// Get the start time for the animation
let startTime = System.DateTime.Now
let lastTimeOption = ref None
let ComputeGravitationalAcceleration (obj:Planet) (obj2:Planet) =
let dx = (obj2.X-obj.X)*m_per_AU
let dy = (obj2.Y-obj.Y)*m_per_AU
let d2 = (dx*dx) + (dy*dy)
let d = sqrt d2
let g = obj.Mass * obj2.Mass * G /d2
let ax = (dx / d) * g / obj.Mass
let ay = (dy / d) * g / obj.Mass
ax,ay
/// Find all the gravitational objects in the system except the given object
let FindObjects(obj) =
[ for paintObject in paintObjects do
match paintObject with
| :? Planet as p2 when p2 <> obj ->
yield p2
| _ ->
yield! [] ]
member sim.Step(time:TimeSpan) =
match !lastTimeOption with
| None -> ()
| Some(lastTime) ->
for paintObject in paintObjects do
match paintObject with
| :? Planet as obj ->
let timeStep = (time - lastTime).TotalSecondsTyped |> realTimeToModelTime
obj.X <- obj.X + timeStep * obj.VX
obj.Y <- obj.Y + timeStep * obj.VY
// Find all the gravitational objects in the system
let objects = FindObjects(obj)
// For each object, apply its gravitational field to this object
for obj2 in objects do
let (ax,ay) = ComputeGravitationalAcceleration obj obj2
obj.VX <- obj.VX + timeStep * ax * AU_per_m
obj.VY <- obj.VY + timeStep * ay * AU_per_m
| _ -> ()
lastTimeOption := Some time
member sim.Start() =
async { while true do
let time = System.DateTime.Now - startTime
// Sleep a little to give better GUI updates
do! System.Threading.Thread.AsyncSleep(1)
sim.Step(time) }
|> Async.Spawn
let s = Simulator().Start()
let massOfEarth = 5.9742e24<kg>
let massOfMoon = 7.3477e22<kg>
let massOfMercury = 3.3022e23<kg>
let massOfVenus = 4.8685e24<kg>
let massOfSun = 1.98892e30<kg>
let mercuryDistanceFromSun = 57910000.0<km> * AU_per_km
let venusDistanceFromSun = 0.723332<AU>
let distanceFromMoonToEarth =384403.0<km> * AU_per_km
let orbitalSpeedOfMoon = 1.023<km/s> * AU_per_km
let orbitalSpeedOfMecury = 47.87<km/s> * AU_per_km
let orbitalSpeedOfVenus = 35.02<km/s> * AU_per_km
let orbitalSpeedOfEarth = 29.8<km/s> * AU_per_km
let sun = new Planet(ipx=1.1<AU>,
ipy=1.1<AU>,
ivx=0.0<AU/s>,
ivy=0.0<AU/s>,
brush=Brushes.Yellow,
mass=massOfSun,
width=20,
height=20)
let mercury = new Planet(ipx=sun.X+mercuryDistanceFromSun,
ipy=sun.Y,
ivx=0.0<AU/s>,
ivy=orbitalSpeedOfMecury,
brush=Brushes.Goldenrod,
mass=massOfMercury,
width=10,
height=10)
let venus = new Planet(ipx=sun.X+venusDistanceFromSun,
ipy=sun.Y,
ivx=0.0<AU/s>,
ivy=orbitalSpeedOfVenus,
brush=Brushes.BlanchedAlmond,
mass=massOfVenus,
width=10,
height=10)
let earth = new Planet(ipx=sun.X+1.0<AU>,
ipy=sun.Y,
ivx=0.0<AU/s>,
ivy=orbitalSpeedOfEarth,
brush=Brushes.Green,
mass=massOfEarth,
width=10,
height=10)
let moon = new Planet(ipx=earth.X+distanceFromMoonToEarth,
ipy=earth.Y,
ivx=earth.VX,
ivy=earth.VY+orbitalSpeedOfMoon,
brush=Brushes.White,
mass=massOfMoon,
width=2,
height=2)
paintObjects.Add(sun)
paintObjects.Add(mercury)
paintObjects.Add(venus)
paintObjects.Add(earth)
paintObjects.Add(moon)
form.Show()
#if COMPILED
[<STAThread>]
do Application.Run(form)
#endif
中间黄色的是太阳,按距离由近及远分别是水星、金星和地球,地球上的小点儿是月球。
小结
NASA气象卫星意外坠落,原因竟是计量单位转换这样的“小问题”。为编程语言添加对计量单位的支持可以很大程度上避免这样的错误,编程任务也变得更有趣。F#提供了对计量单位的静态检查,并且封装了国际单位制的各个单位和物理常量,另外我们也可以定义自己的单位;在单位之间进行换算也很简单;此外F#还支持计量单位的泛型。作为对NASA气象卫星的纪念,本文最后给出了一个模拟太阳系的例子 :)
【http://strangelights.com/fsharp/wiki/default.aspx/FSharpWiki/HomePage.html】
Robert Pickering是《Foundations of F#》一书的作者,他同时还创建了这个关于F#的Wiki,这里是开始学习F#绝好的地方。
该Wiki目前包含了如下内容:F#编程的笔记,F#的书籍和在线杂志,F#的应用和示例,F#类库,F#的Code Snippets,F#方面的文章以及F#的工具,这是继hubFS之后又一个F#知识的集中地,相信F#爱好者以后学习会更方便的!
先把这些好东东抓过来分享下 :)