F#基础教程 定义类型

      F#的类型系统提供一个可以定义定制类型的特性。所有的F#的类型分为两个类别。第一类是元组或纪录类型。这些类型的可以组合成组合类型(类似于C的结构或C#的类)。第二类是聚合(sum)类型,有时也称为联合(union)类型。 元组是一种快速且方便组合一组值的方式。值由逗号隔开,并可以用一个标识符表示。如下面例子中的第一行。你也可以反向取出组合里的值,如下面的第二和第三行,由逗号分割的标识符在等号的左侧,每个标识符依次从元组中取出一个单一值。如果你想忽略元组中的一个值,你可以使用占位符 _ 告诉编译器你不需要使用该值,如第二和第三行所示。

#light
let pair = true, false
let b1, _ = pair
let _, b2 = pair

      在F#中,元组不同于大多数用户定义类型,你不需要使用关键字显式声明它们。定义一个类型,你可以在类型名前使用type关键字,一个等号,然后是类型的定义。最简单的形式,你可以使用一个别名定义任何现有的类型,包括元组。给单一类型赋予别名并不经常使用,这可能会造成混淆,但给一个元组别名却非常有用,尤其是当你想使用一个有类型约束的元组时。下一个例子演示了如果给一个单一类型一个别名,以及如何给一个元组使用有类型约束的别名。

#light
type Name = string
type Fullname = string * string
let fullNameToSting (x : Fullname) =
      let first, second = x in
      first + " " + second

      记录类型类似于元组,它由多个类型组成一个单一类型。不同的是,其中每个类型对应一个字段。下面的例子说明了定义记录类型的语法。字段的定义放置在括号之间,并用分号隔开。一个字段定义由字段名称,后跟一个冒号,和字段的类型组成。下面定义的类型Organization1 是一个记录类型,其中的字段名是唯一的。这意味着你可以使用简单的语法创建这种类型的一个实例,却不需要在创建时提及类型的名称。要创建一个记录,如下面的rainbow 标识符所示,字段名后等号,然后是字段值;放置于括号({})之间。注意,F#有类型系统,所以无需指名类型名,系统会自动推理出标识符类型。

#light
type Organization1 = { boss : string ; lackeys : string list }


let rainbow =
      { boss = "Jeffrey" ;
      lackeys = ["Zippy"; "George"; "Bungle"] }


type Organization2 = { chief : string ; underlings : string list }
type Organization3 = { chief : string ; indians : string list }


let thePlayers =
      { new Organization2
            with chief = "Peter Quince"
            and underlings = ["Francis Flute"; "Robin Starveling";
            "Tom Snout"; "Snug"; "Nick Bottom"] }
let wayneManor =
      { new Organization3
            with chief = "Batman"
            and indians = ["Robin"; "Alfred"] }

      F#不强制字段名称必须唯一(针对所有的记录类型,而不是在一个类型里),所以有时编译器无法仅靠字段名推理出一个类型。在此情况下,你必须使用指定类型的语法:关键字new,类型名称,关键字with开头,字段定义不变,但用关键字and而不是分号隔开,同样,记录定义放于括号之间。类型Organization2,Organization3和它们的实例thePlayers,wayneManor说明了这一点。(此语法类似于对象表达式,这是一种在F#中创建.NET对象的方式,将在第五章讨论)。一般来说,一个类型定义的范围是从它声明开始到声明它的源代码文件的结束。如果一个类型需要引用其后声明的类型,在同一程序段里是可以的。声明的类型必须在同一个程序段里,彼此相邻并且之间没有值的定义,在第一个之后的类型定义的关键字type替换为关键字and。类型声明在此不同于常规类型的声明方式。在程序段里它们可以引用其它类型,甚至可以互相参照。下面有两个类型的示例,声明在同一个程序段里。如果它们单独声明,recipe 将无法引用ingredient,因为recipe在ingredient声明之前。如果它们使用关键字and来连接 ,recipe可以有一个字段是ingredient类型。为什么recipe要放到最前面,这是程式抽象的表现,在首要位置抽象出主要逻辑,其后是实现的细节。

#light
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 = 250 };
            { ingredientName = "Pine nuts" ; quantity = 250 };
            { ingredientName = "Feta cheese" ; quantity = 250 };
            { ingredientName = "Olive oil" ; quantity = 10 };
            { ingredientName = "Lemon" ; quantity = 1 }] ;
      instructions = "Parboil the green beans for about 7 minutes. Roast the pine
nuts carefully in a frying pan. Drain the beans and place in a salad bowl
with the roasted pine nuts and feta cheese. Coat with the olive oil
and lemon juice and mix well. Serve ASAP." }


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
执行结果:
Green Beans & Pine Nuts
Green beans - 250
Pine nuts - 250
Feta cheese - 250
Olive oil - 10
Lemon - 1
Parboil the green beans for about 7 minutes. Roast the pine
nuts carefully in a frying pan. Drain the beans and place in a salad bowl
with the roasted pine nuts and feta cheese. Coat with the olive oil
and lemon juice and mix well. Serve ASAP.

      这个例子还演示了如何同时声明两个类型,以及如何访问记录中的字段。记录比起元组有一个优势,其内容的存取更容易。只需要 标识符.字段名,如上例中关联的标识符名称,toBuy和instructions。

      记录类型也可以被模式匹配,也就是说,你可以使用模式匹配来匹配记录类型里的字段。下一个例子findDavid函数中,正如你所期望的,检查记录的语法是使用类似于模式匹配的语法来构造。你可以比较一个常量和一个字段,用“字段=常量”。你也可以指定标识符为字段的值,用“字段=标识符”。或者,你可以忽略一个字段,用“字段=_”。第一个规则在find_david函数里的作用,检查记录的her字段是否是"Posh",him字段是赋予标识符x,因此它可以用于规则的后半部分。

#light
type couple = { him : string ; her : string }


let couples =
      [ { him = "Brad" ; her = "Angelina" };
      { him = "Becks" ; her = "Posh" };
      { him = "Chris" ; her = "Gwyneth" };
      { him = "Michael" ; her = "Catherine" } ]


let rec findDavid l =
      match l with
      | { him = x ; her = "Posh" } :: tail -> x
      | _ :: tail -> findDavid tail
      | [] -> failwith "Couldn't find David"


print_string (findDavid couples)
执行结果

Becks

      字段值也可以是函数。由于这种技术主要是配合可变(mutable)状态使用,类似于对象的值,在这里我们不讨论这个,将放到第四章的第一节。

      联合(Union)类型,有时也被称为聚合(sum)类型或discriminated unions,是一种数据方式,其可以有不同的含义或结构。下一个例子定义的类型Volume,其值可以有三种不同的可能--Liter ,UsPint ,ImperialPint。虽然数据结构是相同的,都表示浮点数,但含义完全不同。在算法里混合不同含义的数据是程序常见bug的原因,Volume类型尝试避免此类错误。

      定义一个联合类型使用type关键字,然后是类型名称,然后是一个等号--正如所有的类型定义。接着是不同的构造函数的定义,用竖线分割。其第一个竖线是可选的。一个构造函数有一个大写字母开头的名称,这样可以防止与标识符名称的混淆。名字其后可以选择关键字of 和类型做为的构造函数,组成一个构造函数的多个类型用星号分割。在一个类型里,每一个构造函数的名称必须唯一。如果有几个联合类型的定义,那么它们的构造函数名称可以可以相互重叠,但是,你要小心这样做时,因为进一步的类型注释,需要构造和使用联合类型。

      以下的Volume 类型是有三个构造函数的联合类型。每一个组成是一个单一的float类型。构造联合类型一个新实例的语法是,构造函数名,然后是其类型的值,由逗号分割多个值。或者,你可以将值放到括号里。你可以使用三种不同的Volume构造函数来构造三个不同的标识符,vol1, vol2, 和 vol3。

#light
type Volume =
| Liter of float
| UsPint of float
| ImperialPint of float


let vol1 = Liter 2.5
let vol2 = UsPint 2.5
let vol3 = ImperialPint (2.5)

      要解构联合类型值的基本组成部分,你可以使用模式匹配。在一个联合类型上模式匹配,构造函数组成了基本的模式匹配规则。你并不需要一个完整的模式规则,但如果你不这样做,就必须有一个默认的规则,使用无论一个标识符或一个通配符匹配所有剩余的规则。一个构造函数规则由构造函数名,其后是标识符或通配符来匹配它里面的各种值。以下函数,convertVolumeToLiter,convertVolumeUsPint,和convertVolumeImperialPint,演示此语法。

let convertVolumeToLiter x =
      match x with
      | Liter x -> x
      | UsPint x -> x * 0.473
      | ImperialPint x -> x * 0.568


let convertVolumeUsPint x =
      match x with
      | Liter x -> x * 2.113
      | UsPint x -> x
      | ImperialPint x -> x * 1.201


let convertVolumeImperialPint x =
      match x with
      | Liter x -> x * 1.760
      | UsPint x -> x * 0.833
      | ImperialPint x –> x

 

let printVolumes x =
      printfn "Volume in liters = %f,
in us pints = %f,
in imperial pints = %f"
      (convertVolumeToLiter x)
      (convertVolumeUsPint x)
      (convertVolumeImperialPint x)
printVolumes vol1
printVolumes vol2
printVolumes vol3
执行结果。
Volume in liters = 2.500000,
in us pints = 5.282500,
in imperial pints = 4.400000
Volume in liters = 1.182500,
in us pints = 2.500000,
in imperial pints = 2.082500
Volume in liters = 1.420000,
in us pints = 3.002500,
in imperial pints = 2.500000

      联合和记录两个类型都可以被参数化,参数化一个类型意味着先不确认类型定义内部一个或多个类型,等使用时再确认类型。简单说就是类型的泛型。这类似于本章前面讨论过的变量类型的概念。定义类型时,你必须多一点明确哪些类型是可变的。F#支持两种类型参数的语法。第一种,你可以在关键字type和类型名之间使用类型参数,如下。

#light
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) )

      第二种语法,类型参数在类型名后并包裹在尖括号里,如下。

#light
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#提供的两种不同语法。BinaryTree 使用了OCaml的语法风格,其中类型参数在类型名称之前。Tree类型使用了.NET语法风格,类型参数在类型名后并包裹在尖括号里。为创建和使用一个参数化类型实例的语法与创建和使用一个非参数化类型相同。这是因为编译器会自动推理参数化类型的类型参数。你可以看到下面创建的实例tree1 和tree2和使用它们的函数printBinaryTreeValues 和 printTreeValues。

let rec printBinaryTreeValues x =
      match x with
      | BinaryNode (node1, node2) ->
            printBinaryTreeValues node1;
            printBinaryTreeValues node2
      | BinaryValue x -> print_any x; print_string ", "


let rec printTreeValues x =
      match x with
      | Node l -> List.iter printTreeValues l
      | Value x ->
            print_any x
            print_string ", "


printBinaryTreeValues tree1
print_newline()
printTreeValues tree2
执行结果:

1, 2, 3, 4,
"one", "two", "three", "four",

      你可能已经注意到,虽然我们已经讨论了关于类型的定义,创建实例以及实例的应用,我们还没讨论如何更新它们。这是因为这种类型是无法更新的,这是因为一个值随时间的改变违背了函数式编程的理念。不过,F#也有一些类型是可更新的,我们将在第四章讨论。

posted @ 2011-11-30 12:37  银河系漫游指南  阅读(857)  评论(0编辑  收藏  举报