在程序設計中,經常需要檢查某值是否在一個範圍內,這個範圍我們稱爲模式,比對的過程叫模式匹配。用我們之前學的表達式可以完成,這裏用一種更方便的表達式
-
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語句書寫方式一定和函數定義的方式完全一致。
我覺得這裏並不太好說結果有沒有被存在某個變量裏(其實是把值丟棄了,只要新值,沒有出棧的操作,和存值的道理是一樣的),但是尾遞歸函數的性質倒是看的明明白白。