在程序设计中,经常需要检查某值是否在一个范围内,这个范围我们称为模式,比对的过程叫模式匹配。用我们之前学的表达式可以完成,这里用一种更方便的表达式
-
match表达式
match 测试表达式 with | 模式1 [when条件] ->结果表达式1 | 模式2 [when条件] ->结果表达式2 |...
必须包含所有情况,否则报错,最后可以用_来表示剩下的所有情况。
直接上代码体会。let number_class n = match n with |1|3|5|7|9 ->printf"你输入的是小于10的单数" | _ when n >=8 && n <=12 ->printf"你输入的是8到12的数" | _ ->printf"不在范围内"
第一行的模式匹配较好理解。第二行的刚开始一定要写_因为语法要求必须有模式,when条件倒是可以省的,代表输入其他任何东西,或许是字符,或许是数字,或者是其他的,只要符合后面的条件就可以了(至于为什么不直接用n我也不确定,以后要是还能想起来就来填这个坑)
-
再来一个判断成绩的例子
let factor (n:float) = //定义一个函数来判断 match n with | n when n<0. ->printf"成绩必须大于0" | n when n>=90. && n <=100. ->printf"优" | n when n>=80. && n <90. ->printf"良" | n when n>=60. && n <80. ->printf"中" | n when n>=0. && n <60. ->printf"差" | _ ->printf"输入不正确" printfn"请输入成绩:" let input = System.Console.ReadLine() //input为string类型, 见图1解释(我们需要浮点型) let mutable floatValue = 0. //相当于其他语言的初始化 if System.Double.TryParse(input,&floatValue) then //这个函数是把前面的那个string类型参数转换成后面那个浮点类型参数,最后会有bool类型告诉你是否转换成功 (factor floatValue) //转换成功就调用前面写的函数 else printfn"数据转换错误!" //不成功就打印数据转换错误 System.Console.ReadKey(true) //按任意键结束程序
//请输入成绩:
//67
//中
图1
这里的几个函数都是调用的.NET库,他的特征是方法名后面的参数使用了()。
如果使用F#库,他的特征是方法名后面使用空格分隔参数,就像第一行我们自己写的factor函数一样 -
判断串中是否包含某个特定字符
let ff fd = match fd with |'江' -> true |_ -> false let x22 = String.exists ff "财经大学"
//val ff : fd:char -> bool
//val x22 : bool = false
这段代码,真的要是仔细想,有点麻烦,第一个ff函数的作用是:接收一个字符类型的输入,判断这个字符是不是“江”,是就返回true,不是就返回false。
但是我们来看一下F#库里String.exists函数:
接收两个参数,一个参数是函数,一个是string类型,最后返回bool类型。测试字符串里面的每个字符是否满足预测(这个预测是指前面的函数)。
这样看来ff函数的理解方式就得改变了: String.exists ff “财经大学”
String.exists 把 “财经大学” 拆成一个一个字符,再把这个字符带入 ff,形成ff fd。
并且,形式不能写成ff String.exists “财经大学” 。
还有,String.exists函数返回的bool就是ff函数的bool。 -
匿名函数的模式匹配
有两种,一种没有参数,一种有参数。
- 第一种
funcation | 模式1 [when 条件] ->结果表达式1 | 模式2 [when 条件] ->结果表达式2 |...
- 第二种
例子:fun 参数 -> match 参数 with | 模式1 [when 条件] ->结果表达式1 | 模式2 [when 条件] ->结果表达式2 |...
//您输入的是其他数据 5let filterNumbers = function | 1 | 2 |3 ->printfn"您输入的是1、2、3中的一个" | a ->printfn"您输入的是其他数据 %d " a filterNumbers 5
//val filterNumbers : _arg1:int -> unit
//val it : unit = ()
这样的代码就会报错
我们来看一下原因:
更具体的是:
系统自动判断函数接收的参数是int类型,即使这个参数我们没写,但是我们模式里面的值是int类型,所以系统就自动默认为int类型了。
参数没有出现在匿名函数的模式匹配中时,要求参数类型要与模式定义的类型兼容
- 递归与尾递归
前面讲if…then…的时候。用关键字rec定义了递归函数,这样这个函数就可以调用自己了。现在我们看下用match来实现同样的功能该怎么操作。
-
阶乘:
let rec factorial (x: int) = match x with | 0 ->1 | _ ->(factorial (x - 1)) * x let x1 = factorial 5
//val factorial : x:int -> int
//val x1 : int = 120
同样是需要用rec关键字的。 -
斐波那契数列:
let rec fib x = match x with | x when x<0 -> failwith"x必须大于等于0" //抛出异常 | 1 -> 1 | 2 -> 1 | x -> fib(x - 1) + fib(x - 2) printfn"(fib 2) = %i "(fib 2) printfn"(fib 1) = %i "(fib 1) printfn"(fib 8) = %i "(fib 8) printfn"(fib -2) = %i "(fib -2)
//(fib 2) = 1
//(fib 1) = 1
//(fib 8) = 21
//System.Exception: x必须大于等于0 -
求和
let rec recsum n = match n with | 1 -> 1 | _ -> recsum (n - 1) + n let y = recsum 5
//val recsum : n:int -> int
//val y : int = 15
用这个求和的递归为例,要算5就得算4要算4就得算3…最后算到1的时候,就可以回到2,回到3,最后得出答案了。
底层一定是用栈来实现的,这个还好,如果是其他递归入栈过多呢?会有一个问题,栈溢出。
怎么避免这个问题呢?
我们还是要递归,可是不要这种入栈的操作,有个办法就是尾递归。
尾递归是指在函数最后一行有一条递归函数调用语句。并且这条调用han语句书写方式一定和函数定义的方式完全一致。
先别急着理解这句话,先看改写后的代码: -
尾递归改写求和:
let rec tailrecsum n s = match n with |0 -> s |_ -> tailrecsum(n - 1) (s + n) let x = tailrecsum 5 0
//val tailrecsum : n:int -> s:int -> int
//val x : int = 15
第一、函数的参数开始就不一样了,我们看到这儿多了一个参数,这也就是非要说最后一行语句书写方式一定和函数定义的方式完全一致的原因,因为我们改了函数形式
第二、有了第二个参数后会有哪些变化?为什么就不会发生栈溢出了?
我们分析一下计算过程:
我们看到,由于第二个参数的存在,先前应该不计算的5+4被计算并且保存在参数里面了,后面不需要回过来重新计算了。其实这么说不严谨,我们把s的初值赋为0了,那么应该是先算0+5,再算5+4,再算9+3…
第二个参数的频繁更新确实提高了效率。我们的程序从5开始到0结束,就完成了所有操作,所以尾递归的本质是循环。 -
尾递归改写阶乘:
let factorial (number : int) = let rec aMethod (currentNumber : int ) (result : int ) = match currentNumber with | 1 -> result | _ -> aMethod (currentNumber - 1) (result * currentNumber) match number with | 0 -> 1 | _ ->aMethod(number) (1) let x = factorial 5
//val factorial : number:int -> int
//val x : int = 120
解释一下:拿到一个数,看这个数是不是0,如果是0,就直接返回1,如果不是0,那么就得真正计算阶乘了,多用一个参数储存最后的结果,也是相当于循环。大方向来看,这里用到的函数的嵌套。
- if表达式实现尾递归
先来捋一下学习的东西,我们先学了if表达式,接着学了使用if实现递归;接着学历match表达式,然后学习使用match实现递归;然后发现,这个递归不好,尾递归好一点,就学了尾递归;那if表达式的尾递归如何呢?
- if表达式尾递归求阶乘:
let fact2 x = let rec tf (x,n) = if x = 0 then n else tf(x - 1,x * n) tf(x,1) //见图1 let y = fact2 5
//val fact2 : x:int -> int
//val y : int = 120
这个挺好理解的,我们来看看代码里注释的那行,如果注释掉会怎么样:
每个代码必须有结果,并且let不饿能做为最后的结果,这个知识点之前书上提过,现在重新回忆下。
另:
//val fact2 : x:int -> intlet fact2 x = let rec tf x n = if x = 0 then n else tf (x - 1) (x * n) tf x 1 let y = fact2 5
//val y : int = 120
和之前的代码没有区别,但是写法上,特别是参数的定义上面需要留意一下,我觉得都可以。
- 用辗转相除法求最大公因数.尾递归函数
辗转相除法(又名 欧几里得算法):GCD(M, N) = GCD(N, M % N) 特别的,GCD(M, 0)= M
举例:求54、8的最大公因数。GCD(54, 8) = GCD(8, 6) = GCD(6, 2) = GCD(2, 0)= 2
//val GCD : m:int -> n:int -> intopen System let rec GCD (m:int) (n:int) = match n with | 0 ->Math.Abs(m) | _ ->GCD n (m % n) let x = GCD 54 8
//val x : int = 2
这里也是用尾递归实现的,因为尾递归是指在函数最后一行有一条递归函数调用语句。并且这条调用han语句书写方式一定和函数定义的方式完全一致。
我觉得这里并不太好说结果有没有被存在某个变量里(其实是把值丢弃了,只要新值,没有出栈的操作,和存值的道理是一样的),但是尾递归函数的性质倒是看的明明白白。