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# 从未到达它。 解决此问题的最佳方法是有意地将条件的顺序从最具体到最一般排列。
- 注意初学者:上面的代码包含错误,但不会抛出异常。 这些是最糟糕的错误类型,比抛出异常并使应用程序崩溃的错误更糟糕,因为此错误使我们的程序处于无效状态并默默地继续执行。 这样的错误可能在程序生命周期的早期发生,但可能要很长时间才会显现其影响(可能需要几分钟、几天或几周才能有人注意到错误的行为)。 理想情况下,我们希望错误行为尽可能“接近”其来源,因此如果程序进入无效状态,它应该立即抛出异常。 为了防止此类问题,通常最好设置将所有警告视为错误的编译器标志;然后代码将无法编译,从而在开始时就阻止了问题。