書接前文(話說換了個地方,題圖找不到了,隨便整一個吧,這東西不整不好看,整了更難看)

函數簽名

函數簽名是理解一個函數演算法最直接有效的工具,他可以清晰的看到參數的具體信息和返回值的具體信息,單看一個函數的簽名,我們設置就可以寫大量的測試用例了,即使完全不知道這個函數是幹什麼的。

我們仍然從簡單的例子入手,下邊是一個整數加法演算法的函數簽名:

Int -> Int -> Int

關於類型(可能是Int、Int32、Integer或者Number,這裡我們只要知道他表示一個整數類型即可)

好的,你可能認為的整數加法簽名應該是這樣

(Int, Int) -> Int

很好,輸入參數為2個Int型值,然後返回一個Int型的值;想像下上一章節的柯里化內容,函數簽名默認將所有的函數都自動柯里化,因此實際上和下邊這個的寫法沒有太本質的區別

Int -> Int -> Int

前邊兩個箭頭都是視作輸入,假設你只輸入了一個Int參數呢,這個函數契合下邊這個簽名

Int -> ( Int -> Int )

輸入一個Int型參數,返回一個函數(這個函數接收一個Int型參數,然後返回一個Int型值)

考慮到加法演算法最終要返回一個值,因此最後的那個括弧不能括起來(否則就是返回函數了)

這裡的簽名固化了類型,如果要求泛型怎麼辦?

Number a => a -> a -> a

注意到等號箭頭前面描述了泛型信息,他表示 a是一個Number類型(可能是整數、浮點數等等),泛型給予你更精確的描述函數演算法的能力,假設你有一個Trait或者Interface,給予類型做加法計算的能力,那麼函數簽名可能是這樣的

Addable a => a -> a -> a

加法演算法只關心 a 是否能做加法運算的能力,從這個簽名你可以清晰的理解到這個函數演算法可能是與(且僅與)加法操作相關。

試著理解下下面幾個簡單的函數簽名,你能猜猜函數的演算法實現了什麼?

String -> Int
[a] -> Int
[a] -> a
Order a => [a] -> [a]

看看你猜對了沒

String -> Int ( 計算字元串的長度 )
[a] -> Int (計算數組的長度)
[a] -> a (返回數組的某個元素,例如head、tail、get)
Order a => [a] -> [a] (數組排序)

注意到上述函數都是純函數,因此排序演算法返回的是排序好的數組,而非在原數組上做破壞

關於函數簽名,有興趣的可以搜索 Hindley-Milner類型系統 ,本文只是簡單的介紹一下基本原理。

有了函數簽名,對於組合函數你想必有個更加明確的思路,只要函數g的輸出和函數f的輸入匹配,那麼f和g就可以進行組合(也就是在簽名中用箭頭連接起來)

考察下邊幾個函數

R.head // [a] -> a
R.split(.) // String -> [String]
R.size // String -> Int

將他們的簽名連起來看看

String -> [String] -> String -> Int
中間值需要移除(因為既是返回值又是新的參數)
String -> Int

再將函數組合起來

R.compose(R.size, R.head, R.split(.)) // String -> Int

輸入一個字元串,返回用 . 切分後數組的一個元素的長度

有了函數簽名,對於函數組合是不是可以理解的更加透徹一點,思路也更加清晰?

性能

很遺憾,在Lua中應用函數式編程,性能肯定的是有損失的,特別是可能發生的多次迭代、因不可變性帶來的構造開銷。不過任何時候我們都需要清晰的知道我們想要的是什麼,以及演算法真正實現的底層細節。

對於性能不敏感的區域,我們可以放心的應用函數式編程帶給我們的良好體驗,高效和健壯的開發思路;對於性能敏感的區域,我們肯定要酌情放棄這種便利,使用性能更加良好的演算法進行替代。

總歸來說,Lua本身並不是對函數式編程先天良好支持的,只是他可以作為多範式語言的一種,支持我們用這種思路開發程序代碼。

後續

這部分的探討基本結束了,後續的文章可能我會就 λ演算 做一些入門級的介紹,有興趣可以繼續關注。


推薦閱讀:
相關文章