Overview
我們知道在Scala中是沒有switch語句的,取而代之的是從其他函數式編程語言(Haskell等)中吸收的更加強大的模式匹配功能。除了代替switch語句進行條件分支之外,還能夠進行類型檢查,對象解構等操作,幫助我們擺脫冗長的if判斷。
ch match {
case '+' => println("positive")
case '-' => println("negtive")
case _ => println("default")
}
與傳統的相比switch語句,Scala不僅不需要使用break來防止語句跌入下一個條件分支,case的表達式也更加靈活。除了對值進行匹配之外,還能夠對目標的類型進行判斷。
obj match {
case obj:String => println("string object")
case obj:Int => println("integer object")
}
這里跟 instanceof 判斷類型的效果一樣,在Java中是可以通過Visitor Pattern來實現類似的效果的。不過需要注意的是在匹配泛型對象時,由于JVM在runtime時泛型參數是會被擦拭掉的,所以類似List[String] 的匹配方式,在運行時可能并不會匹配到和你想象中完全一樣的結果。事實上List("A") 和 List(2) 都可以匹配成功。
此外,我們還能夠通過模式匹配來對目標對象進行解構。
def quicksort[T <% Ordered[T]](l: List[T]): List[T] =
l match {
case head :: tail => quicksort(tail.filter { _ < head }) ::: List(head) ::: quicksort(tail.filter { _ > head })
case _ => Nil
}
這是一個簡單的快速排序的例子,由于List對象總是由一個head和一個包含剩下元素的tail對象構成的。因此我們可以通過head::tail
的方式將列表解構成表頭和剩下的元素組成的列表。同理,對于更加復雜的數據結構我們也可以做類似的操作。這樣通過構造器的方式來對不同的情況進行匹配并且能夠直接獲取到需要的值。相比而言,用許多if語句來拼湊和賦值是不是沒有這樣簡單直觀呢?
這些功能背后是提取器機制。在對目標進行模式匹配時,會調用該目標的伴生對象的unapply方法,通過該方法將目標對象傳遞進去,然后從中提取值。例如List能解構成head和tail就是由List伴生對象的unapply方法決定的。
你往往可以把unapply方法當做是apply方法的反向操作因為通常提取得到的這些值就是當初用來構造該對象的。而當你在創建case class的時候,這些方法則會自動創建在你定義的類的伴生對象當中以便你使用。使用字節碼反編譯的工具可以幫助你看到自動生成的代碼。
Pattern Matching in Python
在大概了解了模式匹配的概念之后,我們來看下如何在我們熟悉的Python中山寨這一功能吧:)
顯然由于語言本身的限制,我們沒法把代碼中出現的表達式推遲計算到值真正匹配的時候,把print("test")之類的表達式作為參數傳遞給特定的代碼塊只會導致它們直接被計算出來然后將值傳遞過去而已。
第一個嘗試的方案是將要執行的代碼塊作為字符串參數傳遞過去,然后在匹配到時候才通過Python內置的eval函數進行計算。當然,匹配的pattern也同樣用字符串來表達然后通過一個parser來進行解析。例子的話,就是這樣。
match(val,[
("x:str","len(x)"),
("x:int","x**2"),
])
然而這樣做的話也有不少缺點。首先我們將代碼作為字符串來動態進行求值的時候,就損失了包括IDE的靜態檢查以及其他代碼在執行前的預先優化的可能性了。另外由于實際代碼是在這個模塊內部通過eval執行的緣故,因此如果你需要在遞歸函數中使用這個特性的話,就必須完整的注明調用函數所屬的module,否則就會得到一個未知引用的錯誤。
#Test.py
def quicksort(l):
return match(l,[
("[]","[]"),
("head::xs","Test.quicksort([x for x in l if x <= head]) + [head] + Test.quicksort([x for x in l if x > head])"),
])
因此我們這里也只好注明quicksort方法是從Test文件中引用的。這樣實際負責計算的模塊就能夠通過正則表達式來將代碼塊中的疑似模塊的東西全匹配出來然后import。
def import_module(exp):
matches = re.findall(r"([a-zA-Z_][a-zA-Z0-9_]*)\.", exp)
for module_name in matches:
try:
module = __import__(module_name)
globals()[module_name] = module
except ImportError as e:
pass
顯然這樣做有一點蠢。因此在第二次的嘗試中,我選擇通過decorator的方式對Pattern進行初始化,然后實際匹配的時候會才會根據規則走進對應的函數分支,從而避免了之前的問題,也沒有任何額外需要解析的代碼。
@t(int)
def fuck(obj):
return "Int"
@t(str)
def fuck(obj):
return "String"
@otherwise
def fuck(_):
return "Other"
fuck(1) # "Int"
fuck("1") # "String"
fuck(object()) # "Other"
這樣子是不是清爽了很多呢?不同的decorator對應了不同的匹配模式以及解構的方法,只要在裝飾器初始化函數的時候根據同樣的函數名稱返回同樣的函數對象就可以實現這個功能了。對應的快速排序的代碼是這樣子的了。
@nil
def qs():
return []
@list_head_tail
def qs(x, xs):
return qs([y for y in xs if y <= x]) + [x] + qs([y for y in xs if y > x])
qs([]) # []
qs([3, 1, 4, 2]) # [1, 2, 3, 4]
TODO
目前只能對單個輸入參數的情況進行匹配。目標是能夠像Haskell那樣子的方式。另外由于現在只是通過預先設定好的描述符進行匹配,因此也沒有變量綁定之類的功能。
總的來說還停留在隨便寫寫的階段(連我自己都不會想到去用它)。哪天想起來在更新好了。大概就是這樣。