首页
社区
课程
招聘
[推荐]F#函数式编程
2010-10-23 15:53 5312

[推荐]F#函数式编程

2010-10-23 15:53
5312
F#探险之旅(二):函数式编程(上)
F#系列随笔索引页面
函数式编程范式简介

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版本下可能不能通过编译。

[培训]《安卓高级研修班(网课)》月薪三万计划,掌 握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

收藏
点赞0
打赏
分享
最新回复 (2)
雪    币: 189
活跃值: (11)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
adomore 2010-10-23 15:54
2
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语言编写的类库,或者与非托管的类库进行互操作,它会很有用。
雪    币: 189
活跃值: (11)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
adomore 2010-10-23 15:54
3
0
模式匹配(Pattern Matching)

模式匹配允许你根据标识符值的不同进行不同的运算。有点像一连串的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)

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语句后,就只能匹配非正整数了。对于最简单的情况,我们可以省略第一个“|”:
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

这两个函数说明了如何对元组应用模式匹配,它们的功能是求两个布尔值“或”和“且”运算的结果。在myOr中,从第一、二两个模式规则可以知道b1、b2只要有一个为true,计算结果就是true,否则为false。myOr true false的结果为true,myAnd(true, false)结果为false。
模式匹配的常见用法是对列表进行匹配,事实上,对列表来说,较之if…then…else结构,模式匹配的方式更好。看下面的例子:
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

listOfList是一个列表的列表,两个函数concatenateList和concatenateList2的功能都是将listOfList的元素连接为一个大的列表,只不过一个用模式匹配方式实现,一个使用if…then…else结构实现。可以看到使用模式匹配的代码更为简洁明了。观察concatenateList函数,它处理列表的方式是先取出列表的头元素(head),处理它,然后递归地处理剩余元素,这其实是通过模式匹配方式处理列表的最常见的方式(但不是唯一的方式)。

在F#中,模式匹配还可用在其它地方,在后面的文章中将陆续介绍。

定义类型(Defining Types)

F#的类型系统提供了若干特性,可用来创建自定义类型。所有的类型可分为两类,一是元组(Tuple)或记录(Record),它们类似于C#中的类;二是Union类型,有时称为Sum类型。下面分别来看一下它们的特点。

元组是任意对象的有序集合,通过它我们可以快速、方便地将一组值组合在一起。创建之后,就可以引用元组中的值。
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"
  
print_string(findDavid couples)

首先创建了couple类型的列表,findDavid函数将对该列表进行模式匹配,可以将字段与常量值比较,如her = “Posh”;将字段值赋给标识符,如him = x;还可以使用“_”忽略某个字段的值。最后上面例子的打印结果:Becks。

字段值也可以是函数,这种技术将在本系列文章的第三部分介绍。

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
  
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关键字来定义异常,可选地,如果异常包含了数据我们应当声明数据的类型,注意是可以包含多种类型数据的:
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()
      
writeToFile()

需要注意的是,由于CLR架构的原因,抛出异常的代价是很昂贵的,因此要谨慎使用。

延迟求值(或惰性求值,Lazy Evaluation)

第一次接触Lazy的东西是iBATIS中的LazyLoad,也就是延迟加载,它并不是在开始时加载所有数据,而是在必要时才进行读取。延迟求值与此类似,除了性能的提升外,还可用于创建无限的数据结构。

延迟求值与函数式编程语言关系密切,其原理是如果一种语言没有副作用,编译器或运行时可随意选择表达式的求值顺序。F#允许函数具有副作用,因此编译器或运行时不能够按随意地顺序对函数求值,可以说F#具有严格的求值顺序或 F#是一种严格的语言。

如果要利用延迟求值的特性,必须要显式地声明哪些表达式的求值需要延迟,这个要使用lazy关键字。如果需要对该表达式求值,则要调用Lazy模块的force函数。在调用force函数的时候,它会计算表达式的值,而所求得的值会被缓存起来,再次对表达式应用force函数时,所得的值其实是缓存中的值。
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

打印结果为:
Code
Force value the first time:
Hello, sixty!
Force value the second time:
游客
登录 | 注册 方可回帖
返回