F# 程式設計/模式匹配基礎
| F# : 模式匹配基礎 |
模式匹配用於控制流程;它允許程式設計師檢視一個值,將其與一系列條件進行比較,並在滿足該條件的情況下執行某些計算。雖然模式匹配在概念上類似於其他語言中的一系列if ... then語句,但 F# 的模式匹配更加靈活和強大。
在高級別術語中,模式匹配類似於此
match expr with
| pat1 -> result1
| pat2 -> result2
| pat3 when expr2 -> result3
| _ -> defaultResult
每個|定義一個條件,->表示“如果條件為真,則返回此值...” _是預設模式,這意味著它匹配任何內容,就像萬用字元一樣。
使用一個實際的例子,很容易使用模式匹配語法計算第 n 個斐波那契數
let rec fib n =
match n with
| 0 -> 0
| 1 -> 1
| _ -> fib (n - 1) + fib (n - 2)
我們可以在 fsi 中試驗此函式
fib 1;;
val it : int = 1
> fib 2;;
val it : int = 1
> fib 5;;
val it : int = 5
> fib 10;;
val it : int = 55
可以將多個返回相同值的條件連結在一起。 例如
> let greeting name =
match name with
| "Steve" | "Kristina" | "Matt" -> "Hello!"
| "Carlos" | "Maria" -> "Hola!"
| "Worf" -> "nuqneH!"
| "Pierre" | "Monique" -> "Bonjour!"
| _ -> "DOES NOT COMPUTE!";;
val greeting : string -> string
> greeting "Monique";;
val it : string = "Bonjour!"
> greeting "Pierre";;
val it : string = "Bonjour!"
> greeting "Kristina";;
val it : string = "Hello!"
> greeting "Sakura";;
val it : string = "DOES NOT COMPUTE!"
模式匹配是如此基本的功能,以至於 F# 有一個簡寫語法,可以使用function關鍵字編寫模式匹配函式
let something = function
| test1 -> value1
| test2 -> value2
| test3 -> value3
這可能並不明顯,但以這種方式定義的函式實際上接受一個輸入。 這是一個備用語法的簡單示例
let getPrice = function
| "banana" -> 0.79
| "watermelon" -> 3.49
| "tofu" -> 1.09
| _ -> nan (* nan is a special value meaning "not a number" *)
雖然看起來函式沒有接受任何引數,但它實際上具有型別string -> float,因此它接受一個string引數並返回一個float。 呼叫此函式的方式與呼叫任何其他函式的方式完全相同
> getPrice "tofu";;
val it : float = 1.09
> getPrice "banana";;
val it : float = 0.79
> getPrice "apple";;
val it : float = nan
您可以向定義新增更多引數
let getPrice taxRate = function
| "banana" -> 0.79 * (1.0 + taxRate)
| "watermelon" -> 3.49 * (1.0 + taxRate)
| "tofu" -> 1.09 * (1.0 + taxRate)
| _ -> nan (* nan is a special value meaning "not a number" *)
val getPrice : taxRate:float -> _arg1:string -> float
呼叫此函式將得到
> getPrice 0.10 "tofu";;
val it : float = 1.199
請注意,呼叫中的附加引數位於與要進行匹配的隱式引數之前。
F# 的模式匹配語法與命令式語言中的“switch 語句”結構略有不同,因為模式匹配中的每個情況都有一個返回值。 例如,fib函式等效於以下 C#
int Fib(int n) => n switch
{
0 => 0,
1 => 1,
_ => Fib(n - 1) + Fib(n - 2)
};
與所有函式一樣,模式匹配只能具有一個返回型別。
模式匹配不是其他語言中switch結構的奇特語法,因為它不一定要與值匹配,它與資料的形狀匹配。
如果 F# 匹配特定模式,它可以自動將值繫結到識別符號。 這在使用備用模式匹配語法時尤其有用,例如
let rec factorial = function
| 0 | 1 -> 1
| n -> n * factorial (n - 1)
變數n是一個模式。 如果factorial函式被呼叫並傳入一個5,則0和1模式將失敗,但最後一個模式將匹配並將值繫結到識別符號n。
- 注意初學者:模式匹配中的變數繫結對初學者來說通常看起來很奇怪,但它可能是 F# 最強大和最有用的功能。 變數繫結用於將資料結構分解為組成部分並允許程式設計師檢查每個部分;但是,資料結構分解對於大多數 F# 初學者來說過於高階,並且很難使用像
int和string這樣的簡單型別來表達這個概念。 本書將在後面的章節中討論如何使用模式匹配分解資料結構。
有時,僅僅將輸入與特定值進行匹配是不夠的;我們可以使用when關鍵字向模式新增過濾器或保護
let sign = function
| 0 -> 0
| x when x < 0 -> -1
| x when x > 0 -> 1
上面的函式返回一個數字的符號:負數為 -1,正數為 +1,0 為 '0'
> sign -55;;
val it : int = -1
> sign 108;;
val it : int = 1
> sign 0;;
val it : int = 0
變數繫結很有用,因為通常需要實現保護。
請注意,F# 的模式匹配從上到下工作:它將一個值與每個模式進行比較,並返回第一個匹配的模式的值。 程式設計師可能會犯錯誤,例如將一般情況放在特定情況之上(這將阻止特定情況被匹配),或者編寫一個不匹配所有可能輸入的模式。 F# 足夠智慧,可以通知程式設計師這些型別的錯誤。
包含不完整模式匹配的示例
> let getCityFromZipcode zip =
match zip with
| 68528 -> "Lincoln, Nebraska"
| 90210 -> "Beverly Hills, California";;
match zip with
----------^^^^
stdin(12,11): warning FS0025: Incomplete pattern matches on this expression.
For example, the value '0' will not be matched
val getCityFromZipcode : int -> string
雖然此程式碼有效,但 F# 會通知程式設計師可能的錯誤。 F# 警告我們是有原因的
> getCityFromZipcode 68528;;
val it : string = "Lincoln, Nebraska"
> getCityFromZipcode 32566;;
Microsoft.FSharp.Core.MatchFailureException:
Exception of type 'Microsoft.FSharp.Core.MatchFailureException' was thrown.
at FSI_0018.getCityFromZipcode(Int32 zip)
at <StartupCode$FSI_0020>.$FSI_0020._main()
stopped due to error
如果模式沒有匹配,F# 將丟擲異常。 解決此問題的明顯方法是編寫完整的模式。
在函式確實具有有限輸入範圍的情況下,最好採用這種風格
let apartmentPrices numberOfRooms =
match numberOfRooms with
| 1 -> 500.0
| 2 -> 650.0
| 3 -> 700.0
| _ -> failwith "Only 1-, 2-, and 3- bedroom apartments available at this complex"
此函式現在將匹配任何可能的輸入,並在無效輸入時失敗並給出說明性的錯誤訊息(這是有道理的,因為誰會租一個負 42 間臥室的公寓?)。
包含未匹配模式的示例
> let greeting name =
match name with
| "Steve" -> "Hello!"
| "Carlos" -> "Hola!"
| _ -> "DOES NOT COMPUTE!"
| "Pierre" -> "Bonjour";;
| "Pierre" -> "Bonjour";;
------^^^^^^^^^
stdin(22,7): warning FS0026: This rule will never be matched.
val greeting : string -> string
由於模式_匹配任何內容,並且由於 F# 從上到下評估模式,因此程式碼永遠不可能到達模式"Pierre"。
這是此程式碼在 fsi 中的演示
> greeting "Steve";;
val it : string = "Hello!"
> greeting "Ino";;
val it : string = "DOES NOT COMPUTE!"
> greeting "Pierre";;
val it : string = "DOES NOT COMPUTE!"
前兩行返回正確的輸出,因為我們定義了"Steve"的模式,而"Ino"沒有。
但是,第三行是錯誤的。 我們有一個"Pierre"的條目,但 F# 從未到達它。 解決此問題的最佳方法是有意地將條件的順序從最具體到最一般排列。
- 注意初學者:上面的程式碼包含錯誤,但不會丟擲異常。 這些是最糟糕的錯誤型別,比丟擲異常並使應用程式崩潰的錯誤更糟糕,因為此錯誤使我們的程式處於無效狀態並默默地繼續執行。 這樣的錯誤可能在程式生命週期的早期發生,但可能要很長時間才會顯現其影響(可能需要幾分鐘、幾天或幾周才能有人注意到錯誤的行為)。 理想情況下,我們希望錯誤行為儘可能“接近”其來源,因此如果程式進入無效狀態,它應該立即丟擲異常。 為了防止此類問題,通常最好設定將所有警告視為錯誤的編譯器標誌;然後程式碼將無法編譯,從而在開始時就阻止了問題。