從 lambda 到函式合成
February 4, 2022Haskell 的函式就是資料,可以傳入函式或從函式傳回,可以的話,在需要傳遞函式的場合中,儘量運用現有的函式,或者從既有函式產生新函式,例如 map (> 3) [1, 2, 3, 4, 5]
,其中運用了部份套用,可接受兩個引數的 >
函式,因為只接受一個引數 3
,(> 3)
傳回了新函式。
然而想取得 list 中元素的絕對值後加 10 傳回,依至今學到的 Haskell 語法,可以的方式之一是:
mapAbsPlusTen :: Num a => [a] -> [a]
mapAbsPlusTen lt = map absPlusTen lt
where absPlusTen x = abs x + 10
使用 lambda 函式
有時候函式本體很簡單,若懶得大費周章地定義個函式,可以使用 lambda 函式:
ghci> map (\x -> abs x + 10) [-10, 20, -30]
[20,30,40]
ghci>
建立 lambda 函式時使用 \
開頭,嗯 … 就當它就像個少了左腳的 λ 符號,緊接著就是參數,而 ->
右邊是函式本體。lambda 函式也可以做模式比對,來將兩個 list 的各元素配對相加吧!
ghci> let lt1 = [1, 2, 3]
ghci> let lt2 = [4, 5, 6]
ghci> zip lt1 lt2
[(1,4),(2,5),(3,6)]
ghci> map (\(a, b) -> a + b) $ zip lt1 lt2
[5,7,9]
ghci>
zip
可以將兩個 list 中的各元素兩兩配對為 tuple,最後收集在新的 list 中,之後的文件會談到 tuple,簡單來說,上頭的 map
接受的 lambda 函式,設定了模式比對,將 list 中各個 tuple 的元素拆解後進行相加。
記得 Haskell 的多參數函式,其實是由單參數函式組成的嗎?如果你一直認真地面對函式的型態定義,應該早就習慣了才對。例如:
isRightTriangle :: (Floating a, Eq a) => a -> a -> a -> Bool
isRightTriangle a b c = a ** 2 + b ** 2 == c ** 2
如果用 lambda 來定義 isRightTriangle
的話,也是蠻有趣的呢!
isRightTriangle :: Float -> Float -> Float -> Bool
isRightTriangle = \a -> \b -> \c -> a ** 2 + b ** 2 == c ** 2
加上括號會比較清楚,實際上就是 \a -> (\b -> (\c -> a ** 2 + b ** 2 == c ** 2))
,這可以解釋部份套用的過程,部份套用時,例如 isRightTriangle 10
,傳回的函式就是 \b -> (\c -> 100 + b ** 2 == c ** 2)
,若令這個傳回值為 isRightTriangleIfATen
,那麼部份套用 isRightTriangleIfATen 20
的傳回值就是 \c -> 500 == c ** 2
。
會使用 lambda 函式,不代表到處都得用上 lambda 函式,儘量利用現有函式,或從現有函式中產生函式,會是比較好的選擇,lambda 函式在使用時應簡短,不影響可讀性,對於複雜的計算,不建議使用 lambda 函式。
函式合成
如果你有個元素為 Int
的 list,現在打算取絕對值後轉為字串的 list,例如,將 [10, -20, -30]
轉換為 ["10", "20", "30"]
,因為現在已經學會了 lambda 函式,可以寫成 map (\x -> show (abs x)) [10, -20, -30]
,或者你會使用 $
函式的話,也可以寫成 map (\x -> show $ abs x) [10, -20, -30]
。
這個例子,似乎沒辦法利用現有的函式,也沒辦法利用部份套用來產生新函式;若是能定義一個 showAbs
函式進行 \x -> show $ abs x
計算,這樣 map showAbs [10, -20, -30]
看起來會好讀一些。
然而,因為 abs x
的結果,直接作為 show
的引數,在 Haskell 中,可以將上式改為 map (show . abs) [10, -20, -30]
,show . abs
這種方式稱為函式合成(Function composition),因為它跟數學上的函數合成類似,根據維基百科 Function composition 條目,若有函數 f : X → Y 與 g : Y → Z,那麼合成的函式 g(f(x)),就可以將 X 對應至 Z,合成的函式可標示為 g 。 f : X → Z,也就是 (g ∘ f)(x) = g(f(x))。
Haskell 的函式合成,也是從既有函式產生新函式的作法之一,如果某函式執行後的結果,會直接成為另一個函式的輸入,就可以使用函式合成的語法改善可讀性;與 $
不同的是,$
僅僅用來改變函式執行順序,而 .
函式會產生新函式。
使用函式合成,最主要還是要考量可讀性,例如方才想取得 list 中元素的絕對值後加 10 傳回的需求,也可以使用函式合成,只是不見得好閱讀:
ghci> map ((+ 10) . abs) [-10, 20, -30]
[20,30,40]
ghci>
Point free 風格
如果有個需求,是要加總某個 list,然後取得絕對值,並轉為字串:
showAbsSumOf :: (Show a, Num a) => [a] -> String
showAbsSumOf lt = show (abs (sum lt))
在這邊看到函式的型態中有 Show
,這是因為能被 show
處理的資料,必須具備 Show
的行為,
像這個函式可以使用 $
來改善可讀性:
showAbsSumOf :: (Show a, Num a) => [a] -> String
showAbsSumOf lt = show $ abs $ sum lt
此時運用函式合成,可以改寫為:
showAbsSumOf :: (Show a, Num a) => [a] -> String
showAbsSumOf lt = (show . abs . sum) lt
這就是連續地合成函式,因為 show . abs . sum
實際上產生了新函式,因為實際上 =
兩側,最右邊都是 lt
,可以改寫為以下:
showAbsSumOf :: (Show a, Num a) => [a] -> String
showAbsSumOf = show . abs . sum
這稱為 Point free 或 Pointless 風格,這類風格想強調的,是在面對這類需求時,可以思考如何組合既有函式產生新函式來滿足需求,當然,還是以可讀性為主,一長串的函式合成,可不見得是好事。