maximum
函數取一組可排序的 List(屬於 Ord Typeclass) 作參數,並回傳其中的最大值。想一想,在命令式風格中這一函數該怎麼實現。極可能你會設一個變量來存儲當前的最大值,而後用循環遍歷該 List,若存在比這個值更大的元素,則修改變量爲這一元素的值。到最後,變量的值就是運算結果。唔!描述如此簡單的算法還頗費了點口舌呢!html
如今看看遞歸的思路是如何:咱們先定下一個邊界條件,即處理單個元素的 List 時,回傳該元素。若是該 List 的頭部大於尾部的最大值,咱們就能夠假定較長的 List 的最大值就是它的頭部。而尾部若存在比它更大的元素,它就是尾部的最大值。就這麼簡單!如今,咱們在 Haskell 中實現它算法
maximum' :: (Ord a) => [a] -> a
maximum' [] = error "maximum of empty list"
maximum' [x] = x
maximum' (x:xs)
| x > maxTail = x
| otherwise = maxTail
where maxTail = maximum' xs
如你所見,模式匹配與遞歸簡直就是天造地設!大多數命令式語言中都沒有模式匹配,因而你就得造一堆 if-else 來測試邊界條件。而在這裏,咱們僅須要使用模式將其表示出來。第一個模式說,若是該 List 爲空,崩潰!就該這樣,一個空 List 的最大值能是啥?我不知道。第二個模式也表示一個邊緣條件,它說, 若是這個 List 僅包含單個元素,就回傳該元素的值。編程
改用 max
函數會使代碼更加清晰。若是你還記得,max
函數取兩個值作參數並回傳其中較大的值。以下即是用 max
函數重寫的 maximun'
數據結構
maximum' :: (Ord a) => [a] -> a
maximum' [] = error "maximum of empty list"
maximum' [x] = x
maximum' (x:xs) = max x (maximum' xs)
太漂亮了!一個 List 的最大值就是它的首個元素與它尾部中最大值相比較所得的結果,簡明扼要。函數式編程
如今咱們已經瞭解了遞歸的思路,接下來就使用遞歸來實現幾個函數. 先實現下 replicate
函數, 它取一個 Int
值和一個元素作參數, 回傳一個包含多個重複元素的 List, 如 replicate 3 5
回傳 [5,5,5]
. 考慮一下, 我以爲它的邊界條件應該是負數. 若是要 replicate
重複某元素零次, 那就是空 List. 負數也是一樣, 不靠譜.函數
replicate' :: (Num i, Ord i) => i -> a -> [a]
replicate' n x
| n <= 0 = []
| otherwise = x:replicate' (n-1) x
在這裏咱們使用了 guard 而非模式匹配, 是由於這裏作的是布林判斷. 若是 n
小於 0 就回傳一個空 List, 不然, 回傳以 x
做首個元素並後接重複 n-1
次 x
的 List. 最後, (n-1)
的那部分就會令函數抵達邊緣條件.測試
Note: Num 不是 Ord 的子集, 表示數字不必定得拘泥於排序, 這就是在作加減法比較時要將 Num 與 Ord 類型約束區別開來的緣由.ui
接下來實現 take
函數, 它能夠從一個 List 取出必定數量的元素. 如 take 3 [5,4,3,2,1]
, 得 [5,4,3]
. 若要取零或負數個的話就會獲得一個空 List. 一樣, 如果從一個空 List中取值, 它會獲得一個空 List. 注意, 這兒有兩個邊界條件, 寫出來:spa
take' :: (Num i, Ord i) => i -> [a] -> [a]
take' n _
| n <= 0 = []
take' _ [] = []
take' n (x:xs) = x : take' (n-1) xs
首個模式辨認若爲 0 或負數, 回傳空 List. 同時注意這裏用了一個 guard 卻沒有指定 部分, 這就表示 若大於 0, 會轉入下一模式. 第二個模式指明瞭若試圖從一個空 List 中取值, 則回傳空 List. 第三個模式將 List 分割爲頭部和尾部, 而後代表從一個 List 中取多個元素等同於令 做頭部後接從尾部取 個元素所得的 List. 假如咱們要從 中取 3 個元素, 試着從紙上寫出它的推導過程
otherwisenxn-1[4,3,2,1]
reverse' :: [a] -> [a]
reverse' [] = []
reverse' (x:xs) = reverse' xs ++ [x]
繼續下去!code
Haskell 支持無限 List,因此咱們的遞歸就沒必要添加邊界條件。這樣一來,它能夠對某值計算個沒完, 也能夠產生一個無限的數據結構,如無限 List。而無限 List 的好處就在於咱們能夠在任意位置將它斷開.
repeat
函數取一個元素做參數, 回傳一個僅包含該元素的無限 List. 它的遞歸實現簡單的很, 看:
repeat' :: a -> [a]
repeat' x = x:repeat' x
zip
取兩個 List 做參數並將其捆在一塊兒。zip [1,2,3] [2,3]
回傳 [(1,2),(2,3)]
, 它會把較長的 List 從中間斷開, 以匹配較短的 List. 用 zip
處理一個 List 與空 List 又會怎樣? 嗯, 會得一個空 List, 這即是咱們的限制條件, 因爲 zip
取兩個參數, 因此要有兩個邊緣條件
zip' :: [a] -> [b] -> [(a,b)]
zip' _ [] = []
zip' [] _ = []
zip' (x:xs) (y:ys) = (x,y):zip' xs ys
前兩個模式表示兩個 List 中若存在空 List, 則回傳空 List. 第三個模式表示將兩個 List 捆綁的行爲, 即將其頭部配對並後跟捆綁的尾部. 用 zip
處理 [1,2,3]
與 ['a','b']
的話, 就會在 [3]
與 []
時觸及邊界條件, 獲得(1,'a'):(2,'b'):[]
的結果,與 [(1,'a'),(2,'b')]
等價.
再實現一個標準庫函數 -- elem
! 它取一個元素與一個 List 做參數, 並檢測該元素是否包含於此 List. 而邊緣條件就與大多數狀況相同, 空 List. 你們都知道空 List 中不包含任何元素, 便沒必要再作任何判斷
elem' :: (Eq a) => a -> [a] -> Bool
elem' a [] = False
elem' a (x:xs)
| a == x = True
| otherwise = a `elem'` xs
這很簡單明瞭。若頭部不是該元素, 就檢測尾部, 若爲空 List 就回傳 False
.
假定咱們有一個可排序的 List, 其中元素的類型爲 Ord Typeclass 的成員. 如今咱們要給它排序! 有個排序算法很是的酷, 就是快速排序 (quick sort), 睿智的排序方法. 儘管它在命令式語言中也不過 10 行, 但在 Haskell 下邊要更短, 更漂亮, 儼然已經成了 Haskell 的招牌了. 嗯, 咱們在這裏也實現一下. 或許會顯得很俗氣, 由於每一個人都用它來展現 Haskell 究竟有多優雅!
它的類型聲明應爲 quicksort :: (Ord a) => [a] -> [a]
, 沒啥奇怪的. 邊界條件呢? 如料,空 List。排過序的空 List 仍是空 List。接下來即是算法的定義:排過序的 List 就是令全部小於等於頭部的元素在先(它們已經排過了序), 後跟大於頭部的元素(它們一樣已經拍過了序)。 注意定義中有兩次排序,因此就得遞歸兩次!同時也須要注意算法定義的動詞爲"是"什麼而非"作"這個, "作"那個, 再"作"那個...這即是函數式編程之美!如何才能從 List 中取得比頭部小的那些元素呢?List Comprehension。好,動手寫出這個函數!
quicksort :: (Ord a) => [a] -> [a]
quicksort [] = []
quicksort (x:xs) =
let smallerSorted = quicksort [a | a <- xs, a <= x]
biggerSorted = quicksort [a | a <- xs, a > x]
in smallerSorted ++ [x] ++ biggerSorted
咱們已經寫了很多遞歸了,也許你已經發覺了其中的固定模式:先定義一個邊界條件,再定義個函數,讓它從一堆元素中取一個並作點事情後,把餘下的元素從新交給這個函數。 這一模式對 List、Tree 等數據結構都是適用的。例如,sum
函數就是一個 List 頭部與其尾部的 sum
的和。一個 List 的積即是該 List 的頭與其尾部的積相乘的積,一個 List 的長度就是 1 與其尾部長度的和. 等等
再者就是邊界條件。通常而言,邊界條件就是爲避免進程出錯而設置的保護措施,處理 List 時的邊界條件大部分都是空 List,而處理 Tree 時的邊界條件就是沒有子元素的節點。
處理數字時也與之類似。函數通常都得接受一個值並修改它。早些時候咱們編寫過一個計算 Fibonacci 的函數,它即是某數與它減一的 Fibonacci 數的積。讓它乘以零就不行了, Fibonacci 數又都是非負數,邊界條件即可以定爲 1,即乘法的單比特。 由於任何數乘以 1 的結果仍是這個數。而在 sum
中,加法的單比特就是 0。在快速排序中,邊界條件和單比特都是空 List,由於任一 List 與空 List 相加的結果依然是原 List。
使用遞歸來解決問題時應當先考慮遞歸會在什麼樣的條件下不可用, 而後再找出它的邊界條件和單比特, 考慮參數應該在什麼時候切開(如對 List 使用模式匹配), 以及在何處執行遞歸.
轉自:http://learnyouahaskell-zh-tw.csie.org/zh-cn/recursion.html