Haskell Tutorial(12)從 lambda 到函式合成


在 Haskell 中,函式就是資料,可以直接傳給函式或從函式中傳回,可以的話,在需要傳遞函式的場合中,儘量運用現有的函式,或者從既有的函式中產生新函式,例如 map (> 3) [1, 2, 3, 4, 5],其中運用了部份套用,可接受兩個引數的 > 函式,因只接受一個引數 3(> 3) 傳回了新的函式。

然而有時候,我們還是得定義新的函式,例如,想取得 List 中元素的絕對值後加 10 傳回,依至今學到的 Haskell 語法,可以的方式之一是:

mapAbsPlusTen :: [Int] -> [Int]
mapAbsPlusTen xs = map absPlus10 xs
    where absPlusTen x = abs x + 10

使用 lambda 函式

既然函式在 Haskell 就是資料,如同 1"Justin" 等資料可以直接存在而無需名稱,那麼函式是不是也可以?當然,上面這個需求可以改用 lambda,臨時建立一個新函式。例如:

mapAbsPlustTen :: [Int] -> [Int]
mapAbsPlusTen xs = map (\x -> abs x + 10) xs

一個 lambda 函式,是由 \ 開頭,嗯 … 就當它就像個少了左腳的 λ 符號 … XD

緊接著就是參數定義,而 -> 右邊就是函式本體。lambda 函式也可以做模式比對,來將兩個 List 的各元素兩兩配對相加吧!

使用 lambda 函式

zip 可以將兩個 List 中的各元素兩兩配對為 Tuple,然後傳回新的 List,上頭的 map 接受的 lambda 函式,設定了模式比對,將各 Tuple 的元素拆解然後進行相加。

還記得在 Haskell 中,多參數函式,其實是由單參數函式來組成的嗎?如果你一直認真地面對函式的型態宣告,應該早就習慣了才對。例如:

isRightTriangle :: Float -> Float -> Float -> 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] 看起來似乎會好讀一些。

實際上,Haskell 中,可以將上式改為 map (show . abs) [10, -20, -30]show .abs函式合成(Function composition)語法,因為它跟數學上的函數合成類似,根據維基百科 Function composition 條目,若有函數 f : X → Y 與 g : Y → Z,那麼合成的函式 g(f(x)),就可以將 X 中的 x 對應至 Z,合成的函式可標示為 g ∘ f : X → Z,定義就是對 X 中所有 x,(g ∘ f)(x) = g(f(x))。

簡單來說,Haskell 中函式合成語法,也是從既有函式產生新函式的作法之一,如果某個函式執行後的結果,會直接成為另一個函式的輸入,就可以用上函式合成的語法,運用的時機就像上面的範例,用函式合成來改善可讀性。

在可讀性的改善上,在〈Haskell Tutorial(4)這裏,那裏,到處都是函式〉看過,使用 $ 是一個作法,不同的是,$ 僅僅是用來改變函式執行順序,而 . 函式會產生新函式,當你會建立一個單參數的函式,而其中用到了括號或 $,也許就是使用函式合成的時機。

實際上,一開始的 mapAbsPlusTen 範例中,也可以使用函式合成,只是好不好讀的問題:

mapAbsPlus10 :: [Int] -> [Int]
mapAbsPlus10 xs = map ((10 +) . abs) xs

Point free 風格

方才提到,當你會建立一個單參數的函式,而其中用到了括號或 $,也許就是使用函式合成的時機,因此,如果有個需求,是要加總某個 List,然後取得絕對值,並轉為字串:

showAbsSumOf :: [Int] -> String
showAbsSumOf xs = show (abs (sum xs))

像這個函式可以使用 $ 來改善可讀性:

showAbsSumOf :: [Int] -> String
showAbsSumOf xs = show $ abs $sum xs

此時運用函式合成,可以改寫為:

showAbsSumOf :: [Int] -> String
showAbsSumOf xs = show . abs . sum xs

這就是連續地合成函式,因為 show . abs . sum 實際上產生了新的函式,〈Haskell Tutorial(5)如喝水般自然的高階函式〉看過,這時因為實際上右邊都是 xs,可以改寫為 Point free 風格:

showAbsSumOf :: [Int] -> String
showAbsSumOf = show . abs . sum

當然,還是以可讀性為主,一長串的函式合成,可不見得是好事。

注意,你不能寫成以下:

showAbsSumOf :: [Int] -> String
showAbsSumOf = show $ abs $ sum

因為如先前提到,$ 僅僅是用來改變函式執行順序,不會生成新函式,以上的寫法會因為型態不符而編譯錯誤。

挑戰 Y Combinator

那麼,來出個題目吧!我寫過一篇〈用 Python 實現 Y Combinator〉,有想要用 Haskell 來挑戰看看嗎?這需要一些額外的資訊,可以參考一下我寫的 Gist:〈Y Combinator in Haskell〉。