原文地址: "Haskell學習 高階函數" 高階函數(higher order function)就是指可以操作函數的函數,即函數可以作為參數,也可以作為返回結果。有了這兩個特性,haskell可以實現許多神奇的效果。 柯里化(Currying) 在haskell中所有的算術運算符都是函數(包括大 ...
原文地址: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) y 。moreThen4 其實就是 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# 就已經引入了 lambda,JavaScript 也在 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 相似的scanl 和 scanr,它們會記錄下累加值的所有狀態到一個 List。
也有 scanl1 和 scanr1。
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的管道,把上面兩個表達式重寫。現在連 ($) (.) 都不需要了,弔炸天了,有木有