Swift中的模式匹配
Swift有一個(gè)很好的特性,那就是模式匹配的擴(kuò)展。模式是用于匹配的規(guī)則值,如switch語句的case,do語句的catch子句,以及if、while、guard、for-in語句的條件。
例如,假設(shè)你想判斷一個(gè)整數(shù)是大于、小于還是等于零,你可以用if-else if-else語句,盡管這并不美觀:
- let x = 10
- if x > 0 {
- print("大于零")
- } else if x < 0 {
- print("小于零")
- } else {
- print("等于零")
- }
用switch語句會(huì)好很多,我理想的代碼是這樣:
- // 偽代碼
- switch x {
- case > 0:
- print("大于零")
- case < 0:
- print("小于零")
- case 0:
- print("等于零")
- }
但模式匹配默認(rèn)并不支持不等式。讓我們看看能不能改變這個(gè)現(xiàn)狀。為了使過程更加清晰,我先忽略>0的情況,用greaterThan(0)來代替它,過后我再來定義這個(gè)操作符。
擴(kuò)展模式匹配
Swift的模式匹配是基于~=操作符的,如果表達(dá)式的~=值返回true則匹配成功。標(biāo)準(zhǔn)庫(kù)自帶四個(gè)~=操作符的重載:一個(gè)用于Equatable,一個(gè)用于Optional,一個(gè)用于Range,一個(gè)用于Interval。這些都不符合我們的需求,盡管Range和Interval很接近了,關(guān)于它們你可以看這篇文章。
所以我們要實(shí)現(xiàn)我們自己的~=。這個(gè)方法的原型是:
- func ~=(pattern: ???, value: ???) -> Bool
我們知道這個(gè)方法必須返回一個(gè)Bool,那正是我們需要的,我們需要知道這個(gè)值是否匹配模式。接下來要問我們自己的是:參數(shù)的類型是什么?
對(duì)于值,我們可以使用Int,這正是我們?cè)谥暗睦又行枰?。但讓我們把它一般化,讓它能夠接受任何類型。在我們的情況里,模式形如greaterThan(001.png)或lessThan(001.png)。更一般化,模式應(yīng)該是一個(gè)方法,一個(gè)能夠?qū)⒅底鳛閰?shù)并返回true或false的方法。值的類型為T,所以模式的類型應(yīng)為T -> Bool:
- func ~=(pattern: T -> Bool, value: T) -> Bool {
- return pattern(value)
- }
現(xiàn)在我們需定義方法greaterThan和lessThan來創(chuàng)建模式。注意不要把模式greaterThan(0)中的0和我們想匹配的值混淆了。greaterThan的參數(shù)是模式的一部分,這個(gè)部分將在第二步中用到。舉個(gè)例子,greaterThan(0) ~= x和greaterThan(0)(x)是一樣的。
我們知道方法greaterThan(0)必須返回一個(gè)方法,這個(gè)方法要能接受一個(gè)值并返回Bool。所以greaterThan必須是一個(gè)方法,接受另一個(gè)值并返回之前方法。我們把參數(shù)限制成Comparable,為了能在實(shí)現(xiàn)中用Swift的>和<操作符:
- func greaterThan(a: T) -> (T -> Bool) {
- return { (b: T) -> Bool in b > a }
- }
這個(gè)方法接受一個(gè)參數(shù),調(diào)用接受不止一個(gè)參數(shù)的方法并返回,像這樣的方法這被稱為Curried functions。(Swift的部分實(shí)例方法就是一種Curried functions)Swift提供了一種特別的語法用于Curried functions,正如它們的名字一樣形象。使用這種語法,我們的方法變成了這樣:
- func greaterThan(a: T)(_ b: T) -> Bool {
- return b > a
- }
- func lessThan(a: T)(_ b: T) -> Bool {
- return b < a
- }
這樣我們有了***個(gè)版本的switch語句:
- switch x {
- case greaterThan(0):
- print("大于零")
- case lessThan(0):
- print("小于零")
- case 0:
- print("等于零")
- default:
- fatalError("不會(huì)發(fā)生")
- }
很不錯(cuò),但看看default,這個(gè)解決方案不能給編譯器任何提示進(jìn)行完整性檢查,所以我們不得不提供一個(gè)default。如果你確定模式覆蓋了每一個(gè)可能的值,在default下調(diào)用fatalError()是一個(gè)不錯(cuò)的主意,這表明這段代碼絕對(duì)不會(huì)執(zhí)行到。
自定義操作符
回想一開始的想法,以及那段偽代碼。理想情況下,我們想用>0和<0取代greaterThan(0)和lessThan(0)。
自定義操作符存在爭(zhēng)議,因?yàn)槠渌x者經(jīng)常不熟悉這些,它們降低了可讀性?;氐轿覀兊睦又?,類似greaterThan(0)則是完全可讀,所以完全可以認(rèn)為不需要自定義操作符。但同時(shí),每個(gè)人都知道>0意味著什么。所以讓我們來嘗試一下,但正如我們將看到的,它不會(huì)很漂亮。
我們自定義的操作符是一元的——它們只有一個(gè)操作數(shù)。同時(shí),它們是前置操作符(而不是后置,那種操作符在操作數(shù)后的)。在一元操作符和操作數(shù)之間不能有空格,因?yàn)镾wift用空格來區(qū)分一元和二元操作符。此外,<不允許用作前置操作符,我們只好用別的東西代替。(>允許前置,但不是允許后置)。
我建議我們使用~>和~<。雖然~>只是非常像箭頭并不理想,但波浪號(hào)暗示了模式匹配操作符~=。其他我可以想出的操作符(如>>和<<)則容易造成混淆。
9月25日更新:我從Nate Cook那了解到操作符~>在標(biāo)準(zhǔn)庫(kù)中已經(jīng)存在。雖然它的實(shí)現(xiàn)都沒有公有,但Nate發(fā)現(xiàn)它是用來增加集合的索引。鑒于此,為一個(gè)完全不同的目的而使用相同的操作符可能不是一個(gè)好主意。你可以選個(gè)別的。
真正的實(shí)現(xiàn)并不重要。我們要做的就只是聲明操作符和實(shí)現(xiàn)方法,這些只是我們已有的方法greaterThan和lessThan的委托:
- prefix operator ~> { }
- prefix operator ~< { }
- prefix func ~>(a: T)(_ b: T) -> Bool {
- return greaterThan(a)(b)
- }
- prefix func ~ Bool {
- return lessThan(a)(b)
- }
這樣,我們的switch語句變成:
- switch x {
- case ~>0:
- print("大于零")
- case ~<0:
- print("小于零")
- case 0:
- print("等于零")
- default:
- fatalError("不會(huì)發(fā)生")
- }
再次提醒,操作符和操作數(shù)之間沒有空格。
這樣已是我們的極限,很接近原始計(jì)劃,但顯然并不***。
9月19日更新:Joseph Lord提醒我,Swift有一個(gè)類似的語法:
- switch x {
- case _ where x > 0:
- print("大于零")
- case _ where x < 0:
- print("小于零")
- case 0:
- print("等于零")
- default:
- fatalError("不會(huì)發(fā)生")
- }
這個(gè)語法,雖然它可能不像我們定制的解決方案那么簡(jiǎn)潔,但絕對(duì)足夠好,因?yàn)槟悴粦?yīng)該為這么一個(gè)簡(jiǎn)單的目的此創(chuàng)建一個(gè)自定義語法。然而,我們的解決方案是一般化的,能在不同的地方應(yīng)用。繼續(xù)往下看。
其他應(yīng)用
順便說一句,這里給出的解決方案是非常一般化的。我們重載的模式匹配操作符~=適用任何T類型和任何接受T類型返回Bool的方法。換句話說,我們的實(shí)現(xiàn)使得pattern ~= value和pattern(value)一樣好用。更進(jìn)一步,switch value { case pattern: ... }和 if pattern(value) { ... }一樣好用。
檢查數(shù)字奇偶性
舉幾個(gè)例子。首先,一個(gè)簡(jiǎn)單的例子說明了其可應(yīng)用性,雖然其實(shí)際意義不大。假設(shè)你有一個(gè)方法isEven用來檢查數(shù)數(shù)字是不是偶數(shù):
- func isEven(a: T) -> Bool {
- return a % 2 == 0
- }
現(xiàn)在:
- switch isEven(x) {
- case true: print("偶數(shù)")
- case false: print("奇數(shù)")
- }
可以變成:
- switch x {
- case isEven: print("偶數(shù)")
- default: print("奇數(shù)")
- }
注意default,下面的代碼無效:
- switch x {
- case isEven: print("偶數(shù)")
- case isOdd: print("奇數(shù)")
- }
- // error: Switch must be exhaustive, consider adding a default clause
匹配字符串
舉一個(gè)更實(shí)際的例子,假設(shè)你想要匹配一個(gè)字符串的前綴或后綴。我們先寫兩個(gè)方法hasPrefix和hasSuffix,它們接受兩個(gè)字符串,并檢查***個(gè)參數(shù)是否是第二個(gè)參數(shù)的前綴/后綴。這些只是現(xiàn)有標(biāo)準(zhǔn)庫(kù)中String.hasPrefix和String.hasSuffix方法的變形,只是使參數(shù)有一個(gè)方便的順序(前綴/后綴***,完整的字符串第二)。如果你經(jīng)常使用Partial Applied Function(偏應(yīng)用方法,缺少部分參數(shù)的方法)并將它們傳遞給其他方法,你會(huì)發(fā)現(xiàn)你常常需要重復(fù)出現(xiàn)參數(shù)來符合被調(diào)用方法的參數(shù)。煩人,但這不難。
- func hasPrefix(prefix: String)(value: String) -> Bool {
- return value.hasPrefix(prefix)
- }
- func hasSuffix(suffix: String)(value: String) -> Bool {
- return value.hasSuffix(suffix)
- }
現(xiàn)在我們可以這樣,在我看來這很容易閱讀了:
- let str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
- switch str {
- case hasPrefix("B"), hasPrefix("C"):
- print("以B或C開頭")
- case hasPrefix("D"):
- print("以D開頭")
- case hasSuffix("Z"):
- print("以Z結(jié)尾")
- default:
- print("其他情況")
- }
結(jié)論
為了解決我們最初的問題,我們提出了一個(gè)一般化的解決方案,它可以解決很多不同的問題。我發(fā)現(xiàn)這種情況經(jīng)常發(fā)生,當(dāng)你將方法看作值來傳遞,它可以用在你通常想不到的地方。這是函數(shù)式編程改進(jìn)可組合性這一說法背后的核心概念之一。
擴(kuò)展Swift的模式匹配系統(tǒng),使其有了新的功能,無論是對(duì)于內(nèi)置類型還是自定義類型,都是極其強(qiáng)大的。一如既往,注意不要把它擴(kuò)展太多。即使一個(gè)自定義的語法看上去比保守的解決方案更為干凈,但對(duì)于那些不熟悉它的人它使代碼更加難讀了。