Haskell學習-高階函式

Jeff.Zhong發表於2018-08-11

原文地址:Haskell學習-高階函式
高階函式(higher-order function)就是指可以操作函式的函式,即函式可以作為引數,也可以作為返回結果。有了這兩個特性,haskell可以實現許多神奇的效果。

柯里化(Currying)

在haskell中所有的算術運算子都是函式(包括大小於等於關係符等),而它們的快捷方式都可以省略運算元(引數)。

(+) 1 2 -- (+) 是需要兩個運算元的函式
> 3

(+1) 2 -- (+1) 是需要左運算元的函式
> 3

(3*) 3 -- (3*) 是需要右運算元的函式
> 6

map (*2) [1,2,3] -- map所有元素 *2 的操作
> [2,4,6]

filter (>3) [2,3,4,5] -- 過濾 >3的元素
> [4,5]

haskell中的函式預設都是字首模式的,也就是:函式名 引數1 引數2 ... 。但幾乎所有擁有兩個引數的函式都有中綴模式,只需要將函式名反引號包起來就可以了:引數1 `函式名` 引數2。因為在某些情況下中綴函式可讀性更好,更符合人們的理解習慣。

5 `div` 3 -- 求餘數
> 1

9 `mod` 7 -- 求模
> 2

'f' `elem` ['a' .. 'z'] -- 是否包含'f'
> True

本質上,Haskell 的所有函式都只有一個引數,那麼我們多個引數的函式又是怎麼回事? 那是因為所有多個引數的函式都是 Curried functions。其實從上面的算術運算函式例子,我們大概就能猜出來了。接著用例項來進驗證一下:

moreThen4 = max 4 -- 最小為4的函式

:t max -- 需要兩個可比較的引數的函式
max :: Ord a => a -> a -> a

:t moreThen4 -- 需要一個可比較的數字的函式
> moreThen4 :: (Ord a, Num a) => a -> a

通過檢視函式的型別可發現,兩個引數的 max 函式其實可以寫成 (max x) ymoreThen4 其實就是 max 函式以不全的引數呼叫後,再建立了一個新的返回函式,該函式是單個引數形式的。
  這和 JavaScript 裡用 閉包 的特性返回函式來實現 柯里化 是一樣一樣的。但在函式式語言當中,函式本來就是一等公民,這事情簡直就是和吃飯睡覺一樣地自然而然。
  我們看起來很怪的函式型別描述 Num a => a -> a -> a ,這下也能理解通了。它表示的是函式取一個數字引數a後,會返回一個需要a型別引數的函式 (Num a) => a -> a ,最後的這個函式再取一個引數a後 ,最終就會回傳a型別的結果。
利用柯里化去掉多餘引數後的函式更加簡潔:

sum' xs = foldl (+) 0 xs
sum' = foldl (+) 0  -- 去掉xs後

maxNum x = foldr max 0 x
maxNum = foldr max 0  -- 去掉x後

Lambda表示式

  lambda 已經不是什麼新鮮事物了, 早在 .NET 4.0時代 C# 就已經引入了 lambdaJavaScript 也在 ES6 中引入。
  編寫匿名的函式,這樣就不需要費力的建立命名函式。因為匿名函式從 lambda 演算而來,所以匿名函式通常也被稱為 lambda 函式。
  在 Haskell 中,匿名函式以反斜槓符號  開始,後跟函式的引數(可以包含模式),而函式體定義在 -> 符號之後。lambda 函式的定義只能有一條語句,同時無法為一個引數設定多個模式,如 [] 和 (x:xs)。

plusOne = \x -> x+1

checkZero = \x -> if x > 0 then "大於0" 
    else if x<0 then "小於0" 
    else "等於0"

摺疊函式

  遍歷列表是一個非常普遍的需求,用摺疊函式代替顯式遞迴進行遍歷明顯更加易於理解和實現。其中 foldl 是左結合,foldr 是右結合,一般右摺疊效率比較高,同時 foldr 也可以用於無限列表,所以應儘量使用 foldr
  摺疊函式呼叫格式: fold 處理函式 初始值(累加值) 需要摺疊的列表
  另外還提供了和 foldl/foldr 相似的 foldl1/foldr1,它們預設使用列表第一項為初始值,所以可以省略初始值。

map' :: Foldable t1 => (t2 -> a) -> t1 t2 -> [a]
map' f = foldr (\x acc -> f x:acc) []

filter' :: Foldable t => (a -> Bool) -> t a -> [a]
filter' f = foldr (\x acc -> if f x then x:acc else acc) []

elem' :: (Foldable t, Eq a) => a -> t a -> Bool
elem' y = foldl (\acc x -> if y==x then True else acc) False

and' :: Foldable t => t Bool -> Bool
and' = foldr1 (\x y->if not y then False else if not x then False else True)

-- 執行
map' (*2) [1,2]
> [2,4]

filter (>2) [1,2,3,4]
> [3,4]

elem' 1 [1,2,3]
> True

and' [True,False,True]
> False

與 foldl 和 foldr 相似的scanlscanr,它們會記錄下累加值的所有狀態到一個 List。
也有 scanl1scanr1

scanl (+) 0 [3,5,2,1]  
> [0,3,8,10,11]  

scanr (+) 0 [3,5,2,1]  
> [11,8,3,1,0]  

還有 foldl'foldl1' 是它們各自惰性實現的嚴格版本。在用 fold 處理較大的 List 時,經常會遇到堆疊溢位的問題。而這罪魁禍首就是 fold 的惰性: 在執行 fold 時,累加器的值並不會被立即更新,而是做一個"在必要時會取得所需的結果"的承諾。每過一遍累加器,這一行為就重複一次。而所有的這堆"承諾"最終就會塞滿你的堆疊。嚴格的 fold 就不會有這一問題,它們不會作"承諾",而是直接計算中間值的結果並繼續執行下去。如果用惰性 fold 時經常遇到溢位錯誤,就應換用它們的嚴格版。

函式組合

$) 叫作函式呼叫符,它的優先順序最低。

 f $ g x => f (g x)

-- 取>2的列表長度
length (filter (>2) [1,2,3,4])
length $ filter (>2) [1,2,3,4] -- 降低優先順序消除括號
> 2

(.) 函式複合運算子,它可以組合函式,併產生新函式,然後傳遞給其它函式。當然我們可以用 lambda 實現,但大多數情況下,使用函式組合無疑更清楚。

(f . g) x => f(g x) 

-- 驗證字串是否為數字
not ( and ( map isDigit $ "12as"))
not . and . map isDigit $ "12as" -- 使用組合消除括號
> True

這兩個運算子是消除括號的神器,有了它們,程式碼的可讀性大大提高。
我們再利用haskell強大的模式匹配能力,改變函式執行方向,改造後的效果類似於unix/linux的管道,把上面兩個表示式重寫。現在連 ($) (.) 都不需要了,吊炸天了,有木有?

-- 讓引數和結果首尾相連,就是這麼簡單
x |> f = f x

-- unix/linux 中的管道?
[1,2,3,4] |> filter (>2) |>length
> 2

"12as" |> map isDigit |> and |> not
> True

參考資料

《HASKELL 趣學指南》
《Real World Haskell》

相關文章