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