命令式至函數式隨記(三)

English

函數式的思考中心就是分解問題,舉例來說,計算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很長時,效能會比較好,這之後有機會再來聊了。

你可能感兴趣的:(Study)