定义类型
F#自定义类型有2种。
- 元组或者记录,即组合多个不同的类型。类似C的结构或者C#中的类。
- 联合类型。
元组和记录类型(Tuple and Record Types)
元组就是用逗号,把几个值放在一起。可以把元组赋值给一个标识符,也可以反过来。如果需要忽略元组中的一个值,可以用'_'告诉编译器,忽略该值,如下面的代码。
let
pair
=
true,
false
let b1, _ = pair
let _, b2 = pair
let b1, _ = pair
let _, b2 = pair
元组不需要用type关键字显式定义。通常定义一个类型,需要用到type关键字,然后是类型名=,用这种方法你可以给一个已经存在的类型起一个别名,特别当你想要是有元组作为类型约束的时候,这招很有用。看下面的例子:
type
Name
=
string
type Fullname = string * string
let fullNameToSting (x: Fullname ) =
let first, second = x in
first + " " + second
type Fullname = string * string
let fullNameToSting (x: Fullname ) =
let first, second = x in
first + " " + second
记录类型和元组类型类似,在一个类型中组合了多个类型。不同点在于纪录类型中,每个字段都是有名字的。
字段定义在括号里,用分号分割。Organization1是一个记录类型,它的字段名是唯一的。这意味着,不需要写出Organization1这个类型,也可以用简便语法创建Organization1的实例,只需要字段签名都一致。
#light
// 定义organization,使用唯一的字段
type Organization1 = { boss: string ; lackeys: string list }
// 创建organization实例。没有显式的提及Organization1类,只靠字段一致,编译器字段推断
let rainbow =
{ boss = "Jeffrey" ;
lackeys = [ "Zippy" ; "George" ; "Bungle" ] }
// 定义2个organizations,有重复的字段名字
type Organization2 = { chief: string ; underlings: list < string > }
type Organization3 = { chief: string ; indians: string list }
// 创建Organization2实例
let ( thePlayers: Organization2 ) =
{ chief = "Peter Quince" ;
underlings = [ "Francis Flute" ; "Robin Starveling" ;
"Tom Snout" ; "Snug" ; "Nick Bottom" ] }
// 创建Organization3实例
let ( wayneManor: Organization3 ) =
{ chief = "Batman" ;
indians = [ "Robin" ; "Alfred" ] }
// 定义organization,使用唯一的字段
type Organization1 = { boss: string ; lackeys: string list }
// 创建organization实例。没有显式的提及Organization1类,只靠字段一致,编译器字段推断
let rainbow =
{ boss = "Jeffrey" ;
lackeys = [ "Zippy" ; "George" ; "Bungle" ] }
// 定义2个organizations,有重复的字段名字
type Organization2 = { chief: string ; underlings: list < string > }
type Organization3 = { chief: string ; indians: string list }
// 创建Organization2实例
let ( thePlayers: Organization2 ) =
{ chief = "Peter Quince" ;
underlings = [ "Francis Flute" ; "Robin Starveling" ;
"Tom Snout" ; "Snug" ; "Nick Bottom" ] }
// 创建Organization3实例
let ( wayneManor: Organization3 ) =
{ chief = "Batman" ;
indians = [ "Robin" ; "Alfred" ] }
F#不强制字段名必须唯一。因此编译器有时候就无法仅仅靠字段推断类型,这时就要用类型标注。
访问类型字段也很简单,用.号就可以了。
#light
// define an organization type
type Organization = { chief: string ; indians: string list }
// create an instance of this type
let wayneManor =
{ chief = "Batman" ;
indians = [ "Robin" ; "Alfred" ] }
// access a field from this type
printfn "wayneManor.chief = %s" wayneManor. chief
// define an organization type
type Organization = { chief: string ; indians: string list }
// create an instance of this type
let wayneManor =
{ chief = "Batman" ;
indians = [ "Robin" ; "Alfred" ] }
// access a field from this type
printfn "wayneManor.chief = %s" wayneManor. chief
Record类型默认是不可变的。对一个习惯于用命令式语言开发的程序来说,听起来record类型没什么用,因为某些地方要不可避免的要改变字段值。出于这个目的,F#提供一个简便语法,通过更新字段创建record的一个副本。要创建这样一个副本,只需要把record类型的名字放在括号中,后面跟with关键字。看下面的例子。
#light
// define an organization type
type Organization = { chief: string ; indians: string list }
// create an instance of this type
let wayneManor =
{ chief = "Batman" ;
indians = [ "Robin" ; "Alfred" ] }
// create a modified instance of this type
let wayneManor ' =
{ wayneManor with indians = [ "Alfred" ] }
// print out the two organizations
printfn "wayneManor = %A" wayneManor
printfn "wayneManor' = %A " wayneManor'
// define an organization type
type Organization = { chief: string ; indians: string list }
// create an instance of this type
let wayneManor =
{ chief = "Batman" ;
indians = [ "Robin" ; "Alfred" ] }
// create a modified instance of this type
let wayneManor ' =
{ wayneManor with indians = [ "Alfred" ] }
// print out the two organizations
printfn "wayneManor = %A" wayneManor
printfn "wayneManor' = %A " wayneManor'
上述例子创建了wayneManor',把Robin值移除了。结果如下:
另一种访问record类型字段的方法是使用模式匹配。看例子:
#light
// type representing a couple
type Couple = { him : string ; her : string }
// list of couples
let couples =
[ { him = "Brad" ; her = "Angelina" } ;
{ him = "Becks" ; her = "Posh" } ;
{ him = "Chris" ; her = "Gwyneth" } ;
{ him = "Michael" ; her = "Catherine" } ]
// function to find "David" from a list of couples
let rec findDavid l =
match l with
| { him = x ; her = "Posh" } :: tail -> x
| _ :: tail -> findDavid tail
| [ ] -> failwith "Couldn't find David"
// print the results
printfn "%A" (findDavid couples )
// type representing a couple
type Couple = { him : string ; her : string }
// list of couples
let couples =
[ { him = "Brad" ; her = "Angelina" } ;
{ him = "Becks" ; her = "Posh" } ;
{ him = "Chris" ; her = "Gwyneth" } ;
{ him = "Michael" ; her = "Catherine" } ]
// function to find "David" from a list of couples
let rec findDavid l =
match l with
| { him = x ; her = "Posh" } :: tail -> x
| _ :: tail -> findDavid tail
| [ ] -> failwith "Couldn't find David"
// print the results
printfn "%A" (findDavid couples )
输出:Becks
联合类型
联合类型是把不同意义,不同结构的数据放在一起的一种手段。定义联合类型,使用type关键字,然后是名字,然后是=。接下来是不同constructor的定义,用竖线|分割,第一个竖线是可选的。 constructor中名称的一个字母必须大写,使得避免constructor名字和标识符名混起来。名称可以选择跟着一个关键字of,这就构成了constructor。多个类型构成的constructor要用*分割。类型中,constructor的名字必须是唯一的。如果定义了几个联合类型,constructor的名字可以重复。但最好不要这样做,因为这样在某些场合要求类型注解。
下例定义了一个Volume类型,它的值有3个不同的意义,liter,US品脱,或者英制品脱,尽管数据的结构都是相同的,都是float,意义却都不同。把数据的意义混合在一个算法里,是常见的导致程序产生bug的原因。Volume类型,某种程度上是要避免这种问题。
#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 )
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 )
创建联合类型实例的语法,就是constructor名字,然后就是类型的值,多个值的话就逗号分割。你也可以把值放在括号里。
要拆解联合类型的基础部分,需要使用模式匹配。当对一个联合类型模式匹配时,constructor构成了模式匹配的一半规则。关于联合类型可以看:
http://en.wikipedia.org/wiki/Union_(computer_science)
http://cnn237111.blog.51cto.com/2359144/884576
具体使用,看下面的例子就能明白了。
#light
// type representing volumes
type Volume =
| Liter of float
| UsPint of float
| ImperialPint of float
// various kinds of volumes
let vol1 = Liter 2.5
let vol2 = UsPint 2.5
let vol3 = ImperialPint 2.5
// some functions to convert between volumes
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
// a function to print a volume
let printVolumes x =
printfn "Volume in liters = %f,
in us pints = %f,
in imperial pints = %f"
(convertVolumeToLiter x )
(convertVolumeUsPint x )
(convertVolumeImperialPint x )
// print the results
printVolumes vol1
printVolumes vol2
printVolumes vol3
// type representing volumes
type Volume =
| Liter of float
| UsPint of float
| ImperialPint of float
// various kinds of volumes
let vol1 = Liter 2.5
let vol2 = UsPint 2.5
let vol3 = ImperialPint 2.5
// some functions to convert between volumes
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
// a function to print a volume
let printVolumes x =
printfn "Volume in liters = %f,
in us pints = %f,
in imperial pints = %f"
(convertVolumeToLiter x )
(convertVolumeUsPint x )
(convertVolumeImperialPint x )
// print the results
printVolumes vol1
printVolumes vol2
printVolumes vol3
运行结果如下:
使用类型参数定义类型
参数化类型意味着在类型的定义内部空出一些类型,在后期由类型使用者确定具体类型。
F#支持2种类型参数化的语法。第一种,把参数化的类型放在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 ) )
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"
]
)
]
)
和变量类型相同,参数的名字都是由单引号(')开头。通常都使用单个字母作为类型名字。如果需要多个参数类型化,你必须用逗号分割。
上述例子BinaryTree类型采用OCaml语法风格,tree类型采用.NET分格。使用哪种语法不影响编译器做类型推断。看一下
看下面的tree1和tree2的例子。
#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) ) // 定义一棵树 type Tree<'a> = | Node of Tree<'a> list | Value of 'a // 创建一棵树 let tree2 = Node( [ Node( [Value "one"; Value "two"] ) ; Node( [Value "three"; Value "four"] ) ] ) // 打印二叉树的递归函数 let rec printBinaryTreeValues x = match x with | BinaryNode (node1, node2) -> printBinaryTreeValues node1 printBinaryTreeValues node2 | BinaryValue x -> printf "%A, " x // 打印树的递归函数 let rec printTreeValues x = match x with | Node l -> List.iter printTreeValues l | Value x -> printf "%A, " x // 打印结果 printBinaryTreeValues tree1 printfn "" printTreeValues tree2
递归的定义类型
F#中,类型只能按照先后顺序引用,比如A类型写在前面,B类型在后面,那么A类型引用B类型就会出错。比如:
type A={
b:B;
}
type B={
name:string;
}
b:B;
}
type B={
name:string;
}
那么就会出错:
那么解决方法就是把B的定义放到A的前面。
但是如果类型要递归定义的话,比如A引用B,B同时引用了A,那么A和B谁写在前面,都会出现无法未定义类型的错误。
type B={ name:string; value:A; } type A={ b:B; }
如果把A写在B前面
type A={ b:B; } type B={ name:string; value:A; }
出现错误:
F#提供专用的语法,递归的定义类型。就是使用and关键字。该类型必须声明在一起,在一个块内。块之间不能有其他赋值定义。
type A={ b:B; } and B={ name:string; value:A; }
这种方法定义的类型不同于常规方法定义的类型。这种类型可以引用块内的其他类型,甚至可以互相引用。下面的例子,就是两个类型互相引用。XmlElement 和XmlTree声明在同一个块内,如果他们分开定义,XmlElement不能够引用XmlTree,因为XmlElement在XmlTree声明了。正式由于它们的定义由and关键字连接,XmlElement类可以有个XmlTree类型的字段。
// represents an XML attribute type XmlAttribute = { AttribName: string; AttribValue: string; } // represents an XML element type XmlElement = { ElementName: string; Attributes: list; InnerXml: XmlTree } // represents an XML tree and XmlTree = | Element of XmlElement | ElementList of list | Text of string | Comment of string | DDD of XmlAttribute | Empty