函數式的思考中心就是分解問題,舉例來說,計算list長度命令式如下:
def length(list):c = 0
for i in list:
c += 1
return c
將之改為函數式是許多介紹函數式的文章會有的範例:
def length(list):
return 0 if list == [] else 1 + length(list[1:])
若傳入list給length,如果是空list,那長度當然是0,如果可以取得首元素則計數為1,然後持續拆解下去至空list為止,很簡單的概念。類 似地,如果想對一組整數作加總呢?如果是命令式可以如下定義:
def sum(list):
acct = list[0]
for i in range(1, len(list)):
acct += list[i]
return acct
正如命令式至函數式隨記(二)談過,使用迴圈循序處理list中元素的問題,基本上都可轉為遞迴解,不必使用計數器,前一個length是個例子,而這邊 的sum可以改為:
def sum(list):
def rsum(lt, at):
return at if lt == [] else rsum(lt[1:], at + lt[0])
return rsum(list, 0)
這邊感覺rsum有點像上面的length,如果把上面的length調整一下:
def length(list):
def rlen(lt, at):
return at if lt == [] else rlen(lt[1:], at + 1)
return rlen(list, 0)
rsum與rlen結構一模一樣,就差在函式名稱與rsum/rlen遞迴時,第二個參數該如何處理。如果寫個通用的foldLeft呢?
def foldLeft(lt, func, at):
return at if lt == [] else foldLeft(lt[1:], func, func(at, lt[0]))
那length就可以寫為:
def length(list):
return foldLeft([1, 2, 3], lambda at, elem: at + 1, 0)
而sum就可以寫成:
def sum(list):
return foldLeft([1, 2, 3], lambda at, elem: at + elem, 0)
foldLeft很好用,可以有一百萬個用法。在Python中有個functools.reduce,就是foldLeft的實現,這在命令式至函數式 隨記(一)中看過實例,基本上用迴圈對list迭代以計算出某值,都可以用foldLeft來作,不過實際運用可能沒像這邊的sum或length那麼清 楚簡單,如命令式至函數式隨記(一)中看過的,要有乾淨點的程式碼,以及對流程的敏感度,例如:
def eval(expr):
stack = []
for c in toPostfix(expr):
if c in "+-*/":
p2 = stack.pop()
p1 = stack.pop()
stack.append({'+': float.__add__,
'-': float.__sub__,
'*': float.__mul__,
'/': float.__floordiv__}[c](p1, p2))
else:
stack.append(float(c))
return stack[-1]
這是命令式的寫法,感覺得出哪邊有foldLeft嗎?在for c in toPostfix(expr)與最後的return stack[-1],簡單來說,迭代expr,最後得到stack尾端值,如果剛開始練習函數式,相信很難看出來,這時建議從簡單的length、sum 等一看就看出來的開始,慢慢就會對這種較複雜的流程有感覺。
那上面怎麼改為使用foldLeft?一開始的stack就告訴你了,初始是從stack為空開始,咦?可是expr不是list嗎?初始不用是list 中的元素,或至少是list元素同型態嗎?誰說的?沒那回事,foldLeft的初始與回傳可以是不同於list元素的任何型態。在這邊,初始與回傳會是list。接下來就是 傳入的函數抽離出來就好了:
from functools import reduce
def eval(expr):
def doStack(stack, c):
if c in "+-*/":
return stack[0:-2] + [
{'+': float.__add__,
'-': float.__sub__,
'*': float.__mul__,
'/': float.__floordiv__}[c](stack[-2], stack[-1])]
else:
return stack + [float(c)]
return reduce(doStack, toPostfix(expr), [])[-1]
上面直接用Python的foldLeft實現reduce來修改了。先前談過,基本上用迴圈對list迭代以計算出某值,都可以用foldLeft來 作,不過不建議著了魔般,什麼都用foldLeft作,要說的話,命令式至函數式隨記(二)中的procExpr也可以用foldLeft作,不過寫完後 並不好讀,foldLeft是為了重用迭代計值的流程,但某些程度上會降低可讀性,使用時兩者間得略為斟酌。
當然,有foldLeft就會有foldRight,可以自己實現看看,foldLeft與foldRight在沒有結合律考量下,是可以互換的,另一個 考量是在某些語言中,list是代數資料型態(Algebraic data type)結構,在這樣的結構下進行list的+串接與cons,會有效能差異,此時若可以使用foldRight與cons,尤其是結果的list很長時,效能會比較好,這之後有機會再來聊了。