F#是一种强类型的函数式编程语言,也就是说F#中的值的类型是在编译时确定的。然而由于类型推断,需要在代码里指明类型的时候并不多,编译器会从代码里推断类型。F#中的基本数据类型(除了.net中的已有的)有元组(tuple), 可区分联合(discriminated union), 记录(record), 数组(array),列表(list),函数(function )和对象(object)。在下面的简要概述,我们将使用 F# interactive,这是一个工具,编译和执行动态输入的文本。
F# interactive可以使用Visual Studio或从F#安装目录运行fsi.exe 。在整个系列文章中,我们也将使用F#的轻量级语法,这使得代码对空格敏感,但简化了很多的语法规则。 要启用轻量级语法的FSI输入以下命令:
> #light;;
两个分号用于结束输入,所以可以在一行输入多个语句。
F#的数据类型
元组
元组是一种组合多个可能是不同类型的值的简单类型,它的长度是在编译时就确定了的,不可以建立长度未知的元组(可以用列表)。我们可以新建一个包含整形和字符串的元组:
F# interactive 将编译器识别到的类型打印在下面。*表示是元组类型。 把值转存到变量 num 和 str的语法通常被称作模式匹配,它的目的是匹配数据类型不同视角的值,对于元组来说,一个视角是一个值(元组类型),另外一个是一对值。模式匹配可以在所有标准F#类型使用,特别是元组,可区分联合和记录。 当一个函数需要返回多个值时用元组很方便,因为它不再需要新建类或者用引用参数(像C#中的out和ref参数)。在一般情况下,如果一个函数很简单、独立或者很可能被用于模式匹配,我会建议使用元组。要返回一个复杂结构的话最好使用记录类型。
可区分联合
这种类型是用来表达一种存储一些可能的选项(在编码时已知)的数据类型。一个例子是抽象语法树: 还是用模式匹配来处理这个类型的值。我们使用match这个F#的关键词,它用来测试值对应几种可能的模式。对于Expr类型,可能的选项有Binary、Variable和Constant 。下面的例子声明可一个函数eval,用来计算给定的表达式(假设getVariableValue 是一个返回Variable 的函数): 用let声明一个函数,rec 表示它是递归的。 I don’t use a term variable known from other programming languages for a reason that will be explained shortly. 可区分联合形成了传统面向对象继承结构的完美补充。在OO继承里基类声明了所有子类重写的方法,意味着很容易添加新的值的类型(添加子类),但是添加新方法就需要修改所有子类。另一方面可区分联合声明了所有的值的类型,意味着添加新方法很容易,但是添加新的值的类型就要修改所有现有的方法。所以可区分联合是在F#里实现观察者模式的更好的方法。
记录
记录可以看作是包含有名字的成员(被称为labels)的元组,当不容易理解元组的成员的意思时用记录更好。另外一个区别是它使用前必须先声明: 最后一行用了一个有趣的关键词with。记录类型是默认不可改变的,所以经常会需要为了修改一个或多个值而创建新的副本。把所有成员都列出来复制是不现实的,这会造成添加新成员很困难。所以F#提供了with做这件事。 记录在很多方面都和类很类似,实际上可以看作是简单的类。记录是不可改变的,具有结构相等性语义,类具有引用相等性语义。当想使用这种行为时(例如字典的keys)通常会用记录。
列表
列表是典型的链表类型。::运算符将元素附加到列表中,[1; 2; 3]或者1::2::3::[]都表示同样的列表。数组是一个.net兼容的可变的类型,它在内存是顺序存储的,所以性能很好。下面的例子声明了一个列表并实现了一个递归函数来计算总和:
我们用let rec 声明了一个递归函数,用模式匹配来测试列表是否为空。注意list是泛型的,在这个例子是list<int>, 因为能用+的默认类型是int。另外函数也可能是泛型的,例如一个返回列表里最后一项的函数不依赖列表的类型,所以可以是泛型的。它的签名是: list<'a> -> 'a。 写递归函数时F#支持的一个重要功能是尾调用(tail-calls)。当函数的最后一个操作是调用函数(包括递归调用它自己)时,运行时丢掉当前的栈帧,因为它不再有用了-被调用函数的结果是即时调用函数的结果。这可以最大限度地减少堆栈溢出的机会。上面的函数可以用尾递归写成这样:
函数
最后要介绍的类型是函数,它也是“函数式编程”命名的来源。它可以像其它类型一样使用,可以作为参数或者返回值(称为高阶函数),也可以作为泛型的类型参数。函数式编程的一个重要能力是可以创建闭包-创建一个捕获当前栈帧的某些值的函数。 下面的例子演示了一个返回一个函数的函数: 我们在createAdder里用fun创建了一个匿名函数。 其实上面的例子可以简化,因为对于多个参数的函数如果只接到前一(多)个参数会返回一个函数,如下: 我们用传递给一个期望两个参数的函数一个参数来创建add10。返回的是一个只有一个参数的函数,调用add10会把这个参数和固定的10传到add。这被称为柯里化(currying )。
F#库里很多方法被实现为高阶函数,写通用代码时把函数作为参数很有用。例如维护列表的标准函数: 这些方法是泛型的(否则就不会那么有用!)。filter 函数的签名是('a -> bool) -> list<'a> -> list<'a>, 它的第二个参数是一个泛型的列表, 第一个是过滤条件,返回过滤后的列表。 函数的签名会告诉我们很多,例如 map 的签名(('a -> 'b) -> list<'a> -> list<'b>) 我们可以看出map 会对第二个参数的每一项调用一个参数的函数,返回处理过的列表。 最后我们看下管道运算符(|>) 以及柯里化会怎样使我们写代码更容易-我们会用到前面的add函数: filter 和map的连续调用很常见并且写在一行很难又不容易阅读。还好我们有管道运算符, |>操作符将左侧的结果作为最后一个参数传递给右侧的函数, 使得我们可以以正常的调用顺序写代码。List.map 那行演示了柯里化的很常见的用法。
函数组合
前向组合运算符(>>)可以组合两个函数,这意味着很容易建立这样一个函数:它有一个参数,被调用时先调用第一个函数,再把结果传给第二个函数。例如(fst是一个返回一个元组第一个成员的函数): 第一个命令里我们组合了两个函数并把一个元组作为参数传给它。组合运算符可以使我们不需要用fun 关键词创建函数。适当的使用它会提高代码的可读性。
表达式和变量作用域
每个 F# 表达式的计算结果都必须是一个值。对于只执行副作用(例如在屏幕上打印或者修改全局变量的状态)的表达式,将使用 unit 类型的值作为返回值。 unit 类型具有单个值,该值由 () 标记指示。分号用于合并两个表达式,其中第一个表达式的返回类型应该是unit 。下面的例子演示了if 怎样被用作表达式: 代码执行if 的true分支,打印字符然后返回one,再把它赋值给res 。 F#里变量的作用域是由let 绑定决定的,并将可以通过它隐藏同名的变量:
本站技术原创栏目文章均为中睿原创或编译,转载请注明:文章来自中睿,本站保留追究责任的权利。