從 lambda 到函式合成

February 4, 2022

Haskell 的函式就是資料,可以傳入函式或從函式傳回,可以的話,在需要傳遞函式的場合中,儘量運用現有的函式,或者從既有函式產生新函式,例如 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 freePointless 風格,這類風格想強調的,是在面對這類需求時,可以思考如何組合既有函式產生新函式來滿足需求,當然,還是以可讀性為主,一長串的函式合成,可不見得是好事。

分享到 LinkedIn 分享到 Facebook 分享到 Twitter