從函數式得到的,並不只是將命令式外觀的程式碼重構為函數式外觀的程式碼,重點在於 對問題思考方式的重構,從而影響演算法的設計。
下面這個程式是個解 排 列組合 的例子:
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來實現最後這個程式,可以看出可讀性與邏輯性會相差甚多。