水管模型
這一章,我們要回到一個問題,到底函式式和過程式的程式設計思路到底在哪裡?我們這裡提供一個形象的比喻。
過程式--屋子物件
過程式思維裡,每一個可變變數、函式/過程指稱的符號,類似告訴你一個屋子的名字。對一個靜態語言來說,我們可能還需要屋子裡只能放什麼東西。然後,我們每一次呼叫一次函式/過程,就是將對對應屋子裡的函式取出來,以及其他屋子裡的物件取出來,按照函式的方式重新整理,把結果放到原先的屋子或者新的屋子裡。(注意這個描述和圖靈機其實是類似的。)
但是,這個屋子可能會更復雜,我們可能有時候會參考別的屋子裡的情況,甚至另一棟房子裡的屋子(其他模組、第三方外掛),或者甚至是天氣和社會新聞(環境變數、硬體)來盤算每一次整理物件的邏輯(即函式)。這個是「屋子-物件」模型中,最讓人困惑的事。如果你天氣不好,或者別的屋子的情況有問題,你的整理物件的規則可能就有很大的問題,而且溯因是困難的。這個我們也在001中介紹過這個問題。
函式式--水管和資料流
在函數語言程式設計中,我們的模型是編造一些列的水管,水管就是函式式中的函式。我們的目標就是事先將各種函式水管給架設好管道系統。然後將水(資料/不可變引數)倒入進去,等待水管的另一頭流出結果就好了。就像下面的圖顯示的那樣:
stateDiagram-v2
[*] --> function
function --> [*]
compose
我們到目前為止,可以想到的最簡單的就是將水管相連。比如,在做文字處理的時候,我們很有可能會有以下的操作。這個就是典型的水管拼接的過程,我們只需要維護分詞、變小寫、刪除stopword、詞幹化這幾個函式即可。
stateDiagram-v2
[*] --> 分詞
分詞 --> 變小寫
變小寫 --> 刪除stopwords
刪除stopwords --> 詞幹化
詞幹化 --> [*]
當然,我們可以一步步把水倒到水管裡,取出來再倒到另一個水管。那何不我們就直接事先幫水管串起來。這個操作也被稱為compose
(用符號\(\circ\)表示),數學表述如下:
$$(f \circ g) x = f(g(x))$$
我們給出一個簡單的Python
實現:
from functools import reduce
def compose(*args):
"""數學中的compose
>>> from fppy.base import compose
>>> compose(lambda x: x+1, lambda x: x**2)(1)
>>> 4
"""
return reduce(lambda f, g: lambda x: f(g(x)), args, lambda x: x)
比如我們就可以把下面的f1
、f2
、f3
給串起來了:
>>> f1 = lambda x: x + 1
>>> f2 = lambda y: y * 2
>>> f3 = lambda z: z / 3
>>> compose(f3, f2, f1)(1)
1.3333333333333333
>>> h(g(f(1)))
1.3333333333333333
不過有時候,compose
的順序會讓人困惑,我個人喜歡下面and_then
的表述:
def and_then(*args):
return reduce(lambda f, g: lambda x: g(f(x)), args)
對我個人而言這樣子會更明確:
>>> and_then(f, g, h)(1)
1.3333333333333333
不過在具體實現裡,我們其實用到了作為引數的函式的概念,這個也是函數語言程式設計中「函式是一等公民」的表現,具體各種水管模式(聽起來我們是長鬍子的法國水管工)我們將在下面的文章中一一展現。