Pattern matching
January 20, 2022在〈Visitor〉曾經談過模式比對(Pattern matching),也談到模式比對這類功能,概念源自於函數式,優先思考資料該具有什麼樣的結構。
資料的結構
從純函數式的典範來看,優先思考資料該具有什麼樣的結構,本來就該如此,畢竟不是物件導向,不是去考量資料本身能具備哪些方法,而是考量拿到資料之後,該怎麼處理它。
〈Visitor〉中談到的揭露結構/型態,或者具體地說,在 Java 中透過 record
類別揭露資料的結構,透過 sealed
限制子類別的型態,其實是限制你不要用物件導向觀點來看待資料罷了,因為 Java 支援物件導向,許多開發者也太容易用物件導向的思維去設計,在面對函數式的元素時,容易不明白其目的與作用,從而用在不適當的場合。
如果一開始就將資料看成是資料,那就自然多了,例如,再度以〈Immutable〉中的這個例子開始吧!
def leng(lt):
count = 0
for _ in lt:
count = count + 1
return count
lt = [1, 3, 2, 5, 8]
length = leng(lt)
〈Immutable〉中談到「上面這個 leng
,以白話來描述,你做了什麼呢?你走訪 lt
,有元素時遞增 1,直到沒有元素為止…」這段描述其實就表示,你知道 lt
的結構會是什麼,也就是它可以循序走訪,那麼循序走訪的意思是?
可以從 lt
中一個一個取出元素,若將 lt
分為已走訪與未走訪部份,那就是每次從未走訪的部份,取出首個元素,而剩下的部份,又是未走訪的清單,這也是後來遞迴版的實作概念:
def leng(lt):
if lt == []:
return 0
else:
return 1 + leng(lt[1:])
lt = [1, 3, 2, 5, 8]
length = leng(lt)
當 lt
是 []
時,表示沒有元素,長度自然是 0,沒什麼好處理的,這表示你也知道,你處理的 lt
在結構上,會有 []
的可能性。
因此,你實際要處理資料,在結構上會有 []
的可能性,當你只有一個元素時,例如,[10]
,首個元素會是 10,未走訪清單會是 []
,這就是你面對的 lt
。
比對結構
現代許多語言,都有模式比對的語法,〈Visitor〉中看過 Java 的做法了,來看看 Python 吧!Python 3.10 以後,可以使用 match/case
,有人說 Python 終於內建 switch
語法了,那完全是個誤解,match/case
是用來做模式比對的,只不過剛好可以涵蓋 switch
的功能罷了。
Python 支援物件導向,Python 的資料本身就是物件,不過,若你想將資料看成是純綷的資料,並且清楚資料的結構組成的話,那麼搭配 match/case
,就是自然不過的一件事:
def leng(lt):
match lt:
case []:
return 0
case [_, *tail]:
return 1 + leng(tail)
lt = [1, 3, 2, 5, 8]
length = leng(lt)
上面的計算不會用到首元素,因此變數命名為 _
,在慣例上,這表示用不到這個變數,來看看〈Immutable〉中這段命令式語法的範例:
lt = [1, 3, 2, 5, 8]
count = 0
for _ in lt:
count = count + 1
doubled_lt = []
for n in lt:
doubled_lt.append(n * 2)
greaterThan5 = []
for n in doubled_lt:
if n > 5:
greaterThan5.append(n)
在第二與第三個迴圈,會用到 n
,那就表示使用 match/case
實現時,會用到首元素:
def leng(lt):
match lt:
case []:
return 0
case [_, *tail]:
return 1 + leng(tail)
def doubleIt(lt):
match lt:
case []:
return [] # 空清單能乘 2 的元素沒有,結果就還是空清單
case [head, *tail]:
return [head * 2] + doubleIt(tail) # 首元素乘 2 並跟處理完的尾清單串起來
def greaterThanFive(lt):
match lt:
case []:
return [] # 空清單大於 5 的清單當然是空清單
case [head, *tail]:
if head > 5:
return [head] + greaterThanFive(tail) # 串起來
else:
return greaterThanFive(tail) # 不理首元素
lt = [1, 3, 2, 5, 8]
count = leng(lt)
doubled_lt = doubleIt(lt)
greaterThan5 = greaterThanFive(doubled_lt)
資料載體
list
只是一種常見的資料結構,如果你想用模式比對概念處理資料,你必須清楚資料的結構,它是樹狀結構嗎?還是有無的結構?或者是有限元素?比方說 tuple
?
你可能想起來,對了!tuple
不是可以拆解嗎?
pt = (10, 2, 3)
x, y, z = pt
確實地,在函數式語言中,tuple
就是一種簡便的資料結構,或說是資料載體,Python 只是借了一些特性來用,像是不可變動、元素有限等,過去的 Python 支援的 tuple
拆解,就是一種簡單、只針對 tuple
的模式比對。
也就是說,有些語言中的拆解之類的語法,像是 JavaScript,其實都具有模式比對的概念,想善用的話,就是用來處理資料載體,而你必須清楚其資料結構。
模式比對的特性,會進入到這類語言中的原因之一,與現在不少應用程式,都會結合 Web 服務,呼叫公開 API,以 JSON 之類的格式交換資料,因為資料本身就是具結構性的格式,欄位都是公開的,通常應用程式中就會設計對應結構的資料載體,而為了要便於擷取資料中特定結構的訊息,也就開始納入了模式比對、拆解之類的語法。
有些語言為了在定義資料載體時,能有個規範或共同語彙,會針對資料載體設計語法或程式庫,例如 Java 的 record
類別,或者是 Python 的 dataclass
。
例如,來配合 Python 的 dataclass
以及 Python 3.10 以後的 match/case
,將〈Visitor〉中的範例實作出來:
from dataclasses import dataclass
import math
@dataclass
class Point:
x: int
y: int
@dataclass
class Rectangle:
center: Point
width: float
height: float
@dataclass
class Circle:
center: Point
radius: float
def area(shape):
match shape:
case Rectangle(_, width, height):
return width * height
case Circle(_, radius):
return radius * radius * math.pi
rect = Rectangle(Point(0, 0), 10, 20)
circle = Circle(Point(0, 0), 10)
print(area(rect))
print(area(circle))
就命令式語言來說,如果你使用了某種資料載體,想對資料載體進行處理,那麼可以留意語言本身,是否提供了模式比對、拆解之類的語法,試著結合函數式的其他概念(例如 Immutable 等)來運用它,或許就可以找到不錯的設計思路。