这篇博客开始讲解元组、列表、序列和选项类型。
- 元组
元组是一些未命名但经过排序的值的分组,这些值可能具有不同的类型。
- 未命名,就是找不到这个东东,它本身就是常量。
- 排序,就是先写什么,后写什么,决定了元组就是什么,和为命名有着相同是意味儿。
- 不同的类型,就是元组本身就是一个常量,这个常量的类型是我们自己写的常量组合体,不同的类型组合成一种类型。
看完这三个特点,我们可以把元组理解为我们自己定义的一种超级常量类型,最原始的常量组合,叫:元组。
- 语法结构
- 元组里面只有一个常量,还不如直接写这个常量,所以不考虑一元的元组。
- 二元元组,就是里面有两个常量的元组:
let x1 = (1,"hello")
- 三元元组,就是里面有三个常量的元组:
let x2 = (1,"hello",true)
他们的函数特征是:
val x1 : int * string = (1, “hello”)
val x2 : int * string * bool = (1, “hello”, true) - 元组嵌套元组
函数特征:let a = 1 let b = 2 let c = "hello" let x = (a,(b,c))
val a : int = 1
val b : int = 2
val c : string = “hello”
val x : int * (int * string) = (1, (2, “hello”))
-
元组分解
先来一个简单的分解:let x = (1,"hello") //待分解元组 let a1,b1 = x //第一种分解方式 let (a2,b2) = x //第二种分解方式 let a3,_ = x //第三种分解方式
val x : int * string = (1, “hello”)
val b1 : string = “hello”
val a1 : int = 1
val b2 : string = “hello”
val a2 : int = 1
val a3 : int = 1
第一种分解方式比较好看,把元组里面的每一个元素分别赋a1和b1。
第二种分解方式相比之下,多了一对括号,这个括号其实代表着原元组里面的括号。
第三种分解方式只取a3的值,其余的值都被赋给了通配符_了,我们不使用它。通配符_只能对应着一个位置。
我学到这里就有很多疑问,如果元组里面有三个元素我们依然这么取值,会出现什么问题?let x = (1,"hello",a) //待分解元组 let a1,b1 = x //第一种分解方式 let (a2,b2) = x //第二种分解方式 let a3,_ = x //第三种分解方式
我们来看看报错信息:
myfsharp1.fs(3,13): error FS0001: 类型不匹配。应为
“int * string”
而给定的是
“int * string * int”
元组具有不同长度的 2 和 3
接下来我给元组里面添了一个常量,变成这样:let x = (1,"hello","a") //待分解元组 let a1,b1,c1 = x //第一种分解方式 let (a2,b2,c2) = x //第二种分解方式 let a3,_ ,_= x //第三种分解方式
结果成功了:
val x : int * string * string = (1, “hello”, “a”)
val c1 : string = “a”
val b1 : string = “hello”
val a1 : int = 1
val c2 : string = “a”
val b2 : string = “hello”
val a2 : int = 1
val a3 : int = 1
原来元组的长度必须和我们分解的时候对应才可以,这说明,我们必须在心里默认已知元组的长度才可以,其实想想也对,元组是个常量,常量我们本来就是知道的量,为什么不能提前知道呢?
接下里还有个问题,这个括号加着有什么意思?
当待分解元组里面没有括号的时候可以随便加括号吗?let x = (1,"hello","a") //待分解元组 let a1,b1,c1 = x //第一种分解方式 let a2,(b2,c2) = x //第二种分解方式 let a3,_ ,_= x //第三种分解方式
报错:
myfsharp1.fs(4,18): error FS0001: 类型不匹配。应为
“int * ('a * 'b)”
而给定的是
“int * string * string”
类型“'a * 'b”与类型“string”不匹配
显然,我们第二种分解方式出现了问题,系统认为加了括号的是一个量,不能当成两个,所以依然不匹配。和上面讲的一样,我们必须默认已知元组长度,现在分解的时候长度不对,结果肯定也不对。
同理let x = ((1,"hello"),"a") //待分解元组 let a1,b1,c1 = x //第一种分解方式 let (a2,b2,c2) = x //第二种分解方式 let a3,_ ,_= x //第三种分解方式
这段代码肯定也不对,因为待分解元组长度为2,分解方法的长度却是3。
myfsharp1.fs(3,16): error FS0001: 类型不匹配。应为
“(int * string) * string * 'a”
而给定的是
“(int * string) * string”
元组具有不同长度的 3 和 2
我们一定要记得,所有的长度必须一一对应,按照这个标准来写一定不会出错。let x = ((1,"hello"),"a") //待分解元组 let (a1,b1),c1 = x //第一种分解方式 let ((a2,b2),c2) = x //第二种分解方式 let (a3,_ ),_= x //第三种分解方式
val x : (int * string) * string = ((1, “hello”), “a”)
val c1 : string = “a”
val b1 : string = “hello”
val a1 : int = 1
val c2 : string = “a”
val b2 : string = “hello”
val a2 : int = 1
val a3 : int = 1
这样来看的话,分解方法二就是把所有的常量放在一起当成一个元组,也就是长度为1的元组而已,理解不同,但是也能取到值。 -
单独取值
上面的方法必须把长度都写完整才能正确取值,有没有不要这么麻烦的?
我们可以使用元组运算符fst和snd取元组的第一个和第二个元素。let x = ((1,"hello"),"a") //待分解元组 let a1 = fst x let b1 = snd x
val x : (int * string) * string = ((1, “hello”), “a”)
val a1 : int * string = (1, “hello”)
val b1 : string = “a”
此时的a1显然是一个二元的元组。
书上的例子和现在写的这个例子都不太好,因为它没有说清楚fst和snd的运用范围。要记住我们知道的长度法则,想用fst和snd运算符必须保证元组只有两个常量。let x = ((1,"hello"),"a",3) //待分解元组 let a1 = fst x let b1 = snd a1
myfsharp1.fs(3,14): error FS0001: 类型不匹配。应为
“(int * string) * string”
而给定的是
“(int * string) * string * int”
元组具有不同长度的 2 和 3
我们也可以通过fst和snd的函数特征发现这个问题。
-
用模式匹配表达式match访问各个元组元素
使用元组作为参数进行运算let add ( a,b ) = a + b let y = add (5,7)
val add : a:int * b:int -> int
val y : int = 12
看第一个结果:add的函数特征,a:int * b:int是指int类型的参数和int类型的参数整体作为一个参数(这样的话我们是不能使用函数部分应用特性的,必须两个值都传入),-> int 代表返回值是int类型。
我们改写一下这个函数作为对比:let add ( a,b ) c = a + b + c let y = add (5,7) 1
结果为:
val add : a:int * b:int -> c:int -> int
val y : int = 13
现在a:int * b:int 是一个元组类型的参数, -> c:int是另外一个参数,-> int是返回结果类型。 -
使用元组模式匹配和尾递归方式求阶乘x!
//尾递归方法求阶乘 let fact x = let rec tailfact (x,n) = match (x,n) with |(0,_)->n |(_,_)->tailfact (x-1,x * n) tailfact (x,1)
val fact : x:int -> int
val y : int = 120
复习一下尾递归,用rec定义一个递归函数,接收一个二元的元组类型参数,匹配这个参数,如果x这个元素是0,就返回n值,如果不是0,就进入下一次调用。
元组里的第一个元素参数是我们要开始计算的值,第二个参数我们用来存放运算结果,递归结束条件就是返回这个结果,也就是(0,)->n返回n,递归体是(,_)->tailfact (x-1,x * n),x-1控制方向,x * n是递归内容。
递归函数定义好了是不先使用的,等到了需要调用的时候才使用。fact 5,把x带入5,递归函数先掠过,到了 tailfact (x,1),我们现在得到了这个元组参数,1只用来占位置,也叫初始化。开始递归, tailfact (5,1)— tailfact (4,51)— tailfact (3,451)— tailfact (2,3451)—tailfact (1,23451)—tailfact (0,123451)—123451=120,递归结束。
我们fact的返回值是看tailfact的,tailfact的返回值是看模式匹配->后面值的,所以返回int类型的值结束。 -
练习:输入两个实数,构造一个复数元组为返回类型
let complex (r:double) (i:double) = (r,i) let x = complex 5. 7.
val complex : r:double -> i:double -> double * double
val x : double * double = (5.0, 7.0)
没有想象的那么复杂,直接加上括号就是元组类型。 -
元组模式匹配
设:有二元元组(学分,课程名),学分为2每周上2节课,学分为3每周上3节课,学分为4每周上4节课,学分为5每周上5节课,其余学分不存在。
求:输入二元元组,求对应课时。let detectTuple course = match course with | (2,val1)->printfn"《%s》学分为2,每周课时为2."val1 | (3,val1)->printfn"《%s》学分为3,每周课时为3."val1 | (4,val1)->printfn"《%s》学分为4,每周课时为4."val1 | (5,val1)->printfn"《%s》学分为5,每周课时为5."val1 | _->printfn" 不存在." detectTuple (2,"数据库") detectTuple (4,"数据结构") detectTuple (10,"信号系统")
《数据库》学分为2,每周课时为2.
《数据结构》学分为4,每周课时为4.
不存在.
val detectTuple : int * string -> unit
val it : unit = ()
course是一个二元元组常量,就用一个单词表示就行,val1是个正儿八经的的占位符,只要前面的数字对,这个里面传来什么都无所谓,我们打印的时候原样输出。 -
OR模式的模式匹配
其实之前的学习我们见过这个模式。OR就是或
的意思,写的条件有一个成立这个模式就成立。在F#语言中用符号|
表示。OR模式要求运算符两侧模式类型必须兼容。
设:有二元元组(学分,课程名称)。
求:若学分在2-6之间的整数,则输出课程设置合理,否则输出不合理。let detectSoreOR (course:int * string) = match course with | (2,_) | (3,_) | (4,_) | (5,_) | (6,_) ->printfn"课程设置合理。" | _->printfn"课程设置不合理。" detectSoreOR (2,"数据库") detectSoreOR (4,"数据结构") detectSoreOR (10,"信号系统")
课程设置合理。
课程设置合理。
课程设置不合理。
val detectSoreOR : int * string -> unit
val it : unit = () -
AND模式的模式匹配
和OR模式类似,AND就是且
的意思,写的条件必须全部成立这个模式才成立。在F#语言中用符号&
表示。AND模式也要求运算符两侧模式类型兼容。
输入一个int * int 的二元元组,判断元组里面是否有0元素let detectZeroAND point = match point with | (0,0) -> printfn "两个元素全为0." | (val1,val2) & (0,_) -> printfn "元组(%d,%d)第一个元素为0."val1 val2 | (val1,val2) & (_,0) -> printfn "元组(%d,%d)第二个元素为0."val1 val2 | _->printfn"两个元素均不为0。" detectZeroAND (0,0) detectZeroAND (1,0) detectZeroAND (0,1) detectZeroAND (1,1)
两个元素全为0.
元组(1,0)第二个元素为0.
元组(0,1)第一个元素为0.
两个元素均不为0。
val detectZeroAND : int * int -> unit
val it : unit = ()
这个程序没有意思,就是为了运用&
故意用的,不使用时照样能实现一样的功能,注意看不同点。但是我们要学习这个占位符的意义,它和&在一起也是为了达到取值的效果。let detectZeroAND point = match point with | (0,0) -> printfn "两个元素全为0." | (0,_) -> printfn "第一个元素为0." | (_,0) -> printfn "第二个元素为0." | _->printfn"两个元素均不为0。" detectZeroAND (0,0) detectZeroAND (1,0) detectZeroAND (0,1) detectZeroAND (1,1)
两个元素全为0.
第二个元素为0.
第一个元素为0.
两个元素均不为0。
val detectZeroAND : int * int -> unit
val it : unit = ()