标识符也可用于引用函数,在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 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
使用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; }
定义函数的时候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)
类似于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)
通常情况下,我们不需要显式地声明类型,编译器会尝试从值的文字值或调用的函数返回类型来判断其类型,这个过程称为类型推导。可在编译时使用-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
模式匹配允许你根据标识符值的不同进行不同的运算。有点像一连串的if...else结构,也像C++和C#中的switch,但是它更为强大和灵活。
看下面Lucas序列的例子,Lucas序列定义跟Fibonacci序列一样,只不过起始值不同:
Code
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)
匹配规则时按照它们定义的顺序,而且模式匹配必须完整定义,也就是说,对于任意一个可能的输入值,都有至少一个模式能够满足它(即能处理它);否则编译器会报告一个错误信息。另外,排在前面的规则不应比后面的更为“一般”,否则后面的规则永远不会得到匹配,编译器会报告一个警告信息,这种方式很像C#中的异常处理方式,在捕获异常时,我们不能先不会“一般”的Exception异常,然后再捕获“更具体”的NullReferenceException异常。
可以为某个模式规则添加一个when卫语句(guard),可将when语句理解为对当前规则的更强的约束,只有when语句的值为true时,该模式规则才算匹配。在上例的第一个规则中,如果没有when语句,那么任意整数都能够匹配模式,加了when语句后,就只能匹配非正整数了。对于最简单的情况,我们可以省略第一个“|”:
Code
let boolToString x =
match x with false -> "False" | _ -> "True"
这个例子中包含两个模式规则,“_”可以匹配任意值,因此当x值为false时匹配第一个规则,否则就匹配第二个规则。
另一个有用的特性是,我们可以合并两个模式规则,对它们采取相同的处理方式,这个就像C#中的switch…case结构中可以合并两个case一样。
Code
let stringToBool x =
match x with
| "T" | "True" | "true" -> true
| "F" | "False" | "false" -> false
| _ -> failwith "Invalid input."
在本例中,我们把三种模式规则合并在了一起,将字符串值转换为相应的布尔值。
可以对大多数F#中定义的类型进行模式匹配,下面的例子就展示了对元组进行匹配。
Code
let myOr b1 b2 =
match b1, b2 with
| true, _ -> true
| _, true -> true
| _ -> false
let myAnd p =
match p with
| true, true -> true
| _ -> false
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
元组是任意对象的有序集合,通过它我们可以快速、方便地将一组值组合在一起。创建之后,就可以引用元组中的值。
Code
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关键字了,最简单的情况是给已有类型起个别名:
Code
type Name = string
// FirstName, LastName
type FullName = string * string
对于Name类型来说,它仅仅是string类型的别名,FullName则是元组类型的别名。
记录(Record)类型与元组有相似之处,它也是将多个类型的值组合为同一类型,不同之处在于记录类型中的字段都是有名称的。看下面的例子:
Code
type Organization = { Boss : string; Lackeys : string list }
let family =
{ Boss = "Children";
Lackeys = ["Wife"; "Husband"] }
第一行是创建Organization类型,第二行则是创建它的实例,令人惊奇的是不需要声明实例的类型,F#编译器能够根据字段名推导出它的类型。这个功能是很强大,但是F#不强求每个类型的字段都是不同的,如果两个类型的各个字段名都一样怎么办呢?这时可以显式地声明类型:
Code
type Company = { Boss : string; Lackeys : string list }
let myCom =
{ new Company
with Boss = "Bill"
and Lackeys = ["Emp1"; "Emp2"] }
一般情况下,类型的作用域从声明处至所在源文件的结束。如果一个类型要使用在它后面声明的类型,可将这两个类型声明在同一个代码块中。类型间用and分隔,看下面食谱的例子:
Code
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
也可以对记录类型应用模式匹配:
Code
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"
Union类型,有时称为sum类型或discriminated union,可将一组具有不同含义或结构的数据组合在一起。可与C语言中的联合或C#中的枚举类比。先来看个例子:
Code
type Volume =
| Liter of float
| UsPint of float
| ImperialPint of float
Volume类型属于Union类型,包含3个数据构造器(Data Constructor),每个构造器都包含单一的float值。声明其实例非常简单:
Code
let vol1 = Liter 2.5
let vol2 = UsPint 2.5
let vol3 = ImperialPint 2.5
事实上,通过Reflector可以看到,Liter、UsPint和ImperialPint是Volume类型的派生类。在将Union类型解析为其基本类型时,我们需要模式匹配。
Code
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
在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"])])
在F#中,异常的定义类似于Union类型的定义,而异常处理的语法则类似于模式匹配。使用exception关键字来定义异常,可选地,如果异常包含了数据我们应当声明数据的类型,注意是可以包含多种类型数据的:
Code
exception SimpleException
exception WrongSecond of int
// Hour, MInute, Second
exception WrongTime of int * int * int
要抛出一个异常,可使用raise关键字。F#还提供了另一种方法,如果仅仅想抛出一个包含文本信息的异常,可以使用failwith函数,该函数抛出一个FailureException类型的异常。
Code
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块来保证文件得以正确地关闭和释放:
Code
let writeToFile() =
let file = System.IO.File.CreateText("test.txt") in
try
file.WriteLine("Hello F# Fans")
finally
file.Dispose()