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

English

從函數式得到的,並不只是將命令式外觀的程式碼重構為函數式外觀的程式碼,重點在於 對問題思考方式的重構,從而影響演算法的設計。


下面這個程式是個解  排 列組合  的例子:
def rotated(list, i, j):
    lt = list[i:j + 1]
    return list[0:i] + lt[-1:] + lt[0:-1] + list[j + 1:]
    
def perm(list, i, colt):
    if i < len(list):
        for j in range(i, len(list)):
            perm(rotated(list, i, j), i + 1, colt)
    else:
        colt.append(list)

colt = []
perm([1, 2, 3, 4], 0, colt)
for list in colt:
    print(list)

形式上,函數式是不會用到迴圈的,那麼就改為:
def rotated(list, i, j):
    lt = list[i:j + 1]
    return list[0:i] + lt[-1:] + lt[0:-1] + list[j + 1:]
    
def perm(list, i, colt):
    def doFor(j):
        if j < len(list):
            perm(rotated(list, i, j), i + 1, colt)
            doFor(j + 1)
    if i < len(list):
        doFor(i)
    else:
        colt.append(list)

colt = []
perm([1, 2, 3, 4], 0, colt)
for list in colt:
    print(list)

在修改到這邊時有些問題,首先那個doFor只是重構時,暫時不知道怎麼命名時亂取的名稱。按照目的來說,這個doFor函式其實是可以取個 rotateAndPermSub之類的名稱,不過這暗示了這個函式同時作了兩件事,這使得很難將函式設計為有傳回值的方式;如果想將doFor放著不 管,那麼也很難將perm改為有傳回值的方式,而只能使用colt收集排列結果。

函數式思考重點就是將問題分解為子問題。剛剛談到,那個doFor函式其實同時處理了兩件事,所以要分解問題的話,這一定是個明顯目標。doFor作的事 有兩個, 旋轉list後某個區段,然後對得到的新串列尾端(tail)繼續排列 , 這個動作會遞迴至要旋轉的區段達到list尾端為止,而且可以看到,perm呼叫了doFor,而doFor又呼叫了perm,兩個都是遞迴,演算上過於 複雜了。

於是重新思考一下,doFor作的事有兩個, 旋轉list某個區段,然後對得到的 新串列尾端(tail)繼續排列 ...旋轉...排列...旋轉...排列...旋轉...排列...那麼如果先得到所有旋轉後的新串列, 再一次對所有新串列尾端進行處理呢?於是先設計一個allRotated:
def allRotated(list):
    def rotatedTo(i):
        return [list[i]] + list[0:i] + list[i + 1:]
    return [rotatedTo(i) for i in range(len(list))]

給allRotated任意list,它的旋轉區段會從0開始,一直旋轉至list尾端為止。這個一旦寫出來,那perm就簡單了,只要遞迴呼叫自己就好 了:
def perm(list):
    if list == []:
        return [[]]
    else:
        lts = allRotated(list)
        return reduce(lambda a, b: a + b,  
            [[[lt[0]] + pl for pl in perm(lt[1:])] for lt in lts])

跟一開始的程式比較可以發現,連確認位置用的索引i都不用了,因為每次都是對旋轉後的串列尾端作排列嘛!Python中只要lt[1:]就可以了。修改過 後的全部程式就是:
from functools import reduce

def allRotated(list):
    def rotatedTo(i):
        return [list[i]] + list[0:i] + list[i + 1:]
    return [rotatedTo(i) for i in range(len(list))]
    
def perm(list):
    if list == []:
        return [[]]
    else:
        lts = allRotated(list)
        return reduce(lambda a, b: a + b,  
            [[[lt[0]] + pl for pl in perm(lt[1:])] for lt in lts])
    
for list in perm([1, 2, 3, 4]):
    print(list)

以函數式思考重構之後,就算回歸命令式,也是清楚許多。例如:
def allRotated(list):
    def rotatedTo(i):
        rotated = []
        rotated.append(list[i])
        rotated.extend(list[0:i])
        rotated.extend(list[i + 1:])
        return rotated

    all = []
    for i in range(len(list)):
        all.append(rotatedTo(i))
    return all

def perm(list):
    pls = []
    if list == []:
        pls.append([])
    else:
        for lt in allRotated(list):
            for tailPl in perm(lt[1:]):
                pl = []
                pl.append(lt[0])
                pl.extend(tailPl)
                pls.append(pl)
    return pls
    
for list in perm([1, 2, 3, 4]):
    print(list)

與一開始命令式的程式比較一下,這個還是清楚多了。雖然用Python回頭這麼作有點無聊,不過對於不若Python具有較多函數式相關元素的程式語言, 像是Java來說就很重要了,用Java來實現一開始看到的那個演算,以及用Java來實現最後這個程式,可以看出可讀性與邏輯性會相差甚多。

你可能感兴趣的:(Study)