另一個 Haskell 教程/語言進階
| Haskell | |
|---|---|
| |
| 另一個 Haskell 教程 | |
| 前言 | |
| 介紹 | |
| 入門 | |
| 語言基礎 (解答) | |
| 型別基礎 (解答) | |
| IO (解答) | |
| 模組 (解答) | |
| 高階語言 (解答) | |
| 高階型別 (解答) | |
| 單子 (解答) | |
| 高階 IO | |
| 遞迴 | |
| 複雜度 | |
我們已經瞭解瞭如何使用 map 將列表中元素的值翻倍
示例
Prelude> map (\x -> x*2) [1,2,3,4] [2,4,6,8]
但是,有一種更簡潔的方式來編寫它
示例
Prelude> map (*2) [1,2,3,4] [2,4,6,8]
這種操作可以針對任何中綴函式進行
示例
Prelude> map (+5) [1,2,3,4] [6,7,8,9] Prelude> map (/2) [1,2,3,4] [0.5,1.0,1.5,2.0] Prelude> map (2/) [1,2,3,4] [2.0,1.0,0.666667,0.5]
你可能會想嘗試透過將 -2 對映到列表上來從列表中的元素中減去值。但這行不通,因為雖然 +2 中的 + 被解析為標準加號運算子(因為沒有歧義),但 -2 中的 - 被解釋為一元減號,而不是二元減號。因此,這裡的 -2 是數字 ,而不是函式 .
一般來說,這些被稱為節。對於二元中綴運算子(如 +),我們可以透過將其括在括號中來使函式成為字首。例如
示例
Prelude> (+) 5 3 8 Prelude> (-) 5 3 2
此外,我們可以提供其任何一個引數來建立一個節。例如
示例
Prelude> (+5) 3 8 Prelude> (/3) 6 2.0 Prelude> (3/) 6 0.5
非中綴函式可以透過將其括在反引號中("`")來變為中綴。例如
示例
Prelude> (+2) `map` [1..10] [3,4,5,6,7,8,9,10,11,12] Prelude> (`map` [1..10]) (+2) [3,4,5,6,7,8,9,10,11,12]
回想一下關於 函式 的部分,有很多計算需要在函式中的多個位置使用同一個計算的結果。在那裡,我們考慮了計算二次多項式根的函式
roots a b c =
((-b + sqrt(b*b - 4*a*c)) / (2*a),
(-b - sqrt(b*b - 4*a*c)) / (2*a))
除了let繫結在那裡介紹,我們也可以使用where子句。where 子句緊隨函式定義之後,並引入了新的佈局級別(參見關於 佈局 的部分)。我們將其寫為
roots a b c =
((-b + det) / (2*a), (-b - det) / (2*a))
where det = sqrt(b*b-4*a*c)
在where子句中定義的任何值都會遮蔽任何其他具有相同名稱的值。例如,如果我們有以下程式碼塊
det = "Hello World"
roots a b c =
((-b + det) / (2*a), (-b - det) / (2*a))
where det = sqrt(b*b-4*a*c)
f _ = det
roots 的值不會注意到 det 的頂級宣告,因為它被區域性定義遮蔽了(型別不匹配也不重要)。此外,由於 f 無法“看到” roots 的內部,所以它唯一知道的關於 det 的資訊是頂級可用的資訊,即字串“Hello World”。
我們也可以提取 2*a 計算並得到以下程式碼
roots a b c =
((-b + det) / (a2), (-b - det) / (a2))
where det = sqrt(b*b-4*a*c)
a2 = 2*a
子表示式在where子句中必須出現在函式定義之後。有時將區域性定義放在函式的實際表示式之前更方便。這可以透過使用let/in子句來完成。我們已經看到了let子句;where子句與其let子句兄弟姐妹幾乎完全相同,只是位置不同。同一個 roots 函式可以使用letas
roots a b c =
let det = sqrt (b*b - 4*a*c)
a2 = 2*a
in ((-b + det) / a2, (-b - det) / a2)
使用where子句,它看起來像
roots a b c = ((-b + det) / a2, (-b - det) / a2)
where
det = sqrt (b*b - 4*a*c)
a2 = 2*a
這兩種型別的子句可以混合使用(即,你可以編寫一個既有let子句又有where子句的函式)。強烈建議不要這樣做,因為它會使程式碼難以閱讀。但是,如果你選擇這樣做,那麼let子句中的值會遮蔽where子句中的值。因此,如果你定義了函式
f x =
let y = x+1
in y
where y = x+2
f 5 的值為 6,而不是 7。當然,我懇求你永遠不要編寫這樣的程式碼。沒有人應該記住這條規則,而且透過遮蔽where-定義的值在let子句中只會讓你的程式碼難以理解。
一般來說,使用let子句還是where子句主要取決於個人喜好。通常,你為子表示式賦予的名稱應該足夠有表達性,這樣即使不閱讀它們的定義,任何閱讀你程式碼的人也能弄清楚它們的功能。在這種情況下,where子句可能更可取,因為它們允許讀者立即看到函式的功能。但是,在現實生活中,值通常被賦予了神秘的名稱。
部分應用是指你將一個接收 個引數的函式,並提供它 個引數。當討論 節 時,我們看到了“部分應用”的一種形式,其中諸如 + 之類的函式被部分應用。例如,在表示式 map (+1) [1,2,3] 中,節 (+1) 是 + 的部分應用。這是因為 + 實際上需要兩個引數,但我們只提供了一個。
部分應用在函式定義中非常常見,有時被稱為“η(eta)歸約”。例如,假設我們要編寫一個函式 lcaseString,該函式將整個字串轉換為小寫。我們可以這樣寫:
lcaseString s = map toLower s
這裡沒有部分應用(儘管你可以爭辯說對 toLower 應用零個引數可以被認為是部分應用)。但是,我們注意到 s 的應用出現在 lcaseString 和 map toLower 的末尾。事實上,我們可以透過執行 eta 歸約來刪除它,得到:
lcaseString = map toLower
現在,我們對 map 進行部分應用:它期望一個函式和一個列表,但我們只提供了一個函式。
這一切都與 map 的型別有關,即 (a -> b) -> ([a] -> [b]),當包含所有括號時。在我們的例子中,toLower 的型別為 Char -> Char。因此,如果我們將此函式提供給 map,我們將得到一個型別為 [Char] -> [Char] 的函式,如預期。
現在,考慮將字串轉換為小寫並刪除所有非字母字元的任務。我們可以這樣寫:
lcaseLetters s = map toLower (filter isAlpha s)
但請注意,我們實際上可以用函式組合來寫:
lcaseLetters s = (map toLower . filter isAlpha) s
同樣,我們得到一個可以進行 eta 歸約的函式:
lcaseLetters = map toLower . filter isAlpha
以這種方式編寫函式在高階 Haskell 使用者中非常常見。事實上,它有一個名字:無點程式設計(不要與無意義程式設計混淆)。之所以稱為無點,是因為在 lcaseLetters 的原始定義中,我們可以將值 s 視為函式起作用的點。透過從函式定義中刪除該點,我們得到了一個無點函式。
類似於 (.) 的函式是 ($)。(.) 是函式組合,($) 是函式應用。($) 在 Prelude 中的定義非常簡單:
f $ x = f x
但是,此函式被賦予了非常低的固定性,這意味著它可以用來替換括號。例如,我們可以編寫一個函式:
foo x y = bar y (baz (fluff (ork x)))
但是,使用函式應用函式,我們可以將其改寫為:
foo x y = bar y $ baz $ fluff $ ork x
這與函式組合語法非常相似。($) 函式在與其他中綴函式組合使用時也很有用。例如,我們不能寫:
示例
Prelude> putStrLn "5+3=" ++ show (5+3)
因為這被解釋為 (putStrLn "5+3=") ++ (show (5+3)),這是沒有意義的。但是,我們可以透過改寫來解決這個問題:
示例
Prelude> putStrLn $ "5+3=" ++ show (5+3)
這可以正常工作。
現在考慮從元組列表中提取所有第一個元件大於零的元組的任務。一種編寫方法是:
fstGt0 l = filter (\ (a,b) -> a>0) l
我們可以首先對整個函式應用 eta 歸約,得到:
fstGt0 = filter (\ (a,b) -> a>0)
現在,我們可以改寫 lambda 函式以使用 fst 函式而不是模式匹配:
fstGt0 = filter (\x -> fst x > 0)
現在,我們可以使用 fst 和 > 之間的函式組合得到:
fstGt0 = filter (\x -> ((>0) . fst) x)
最後,我們可以進行 eta 歸約:
fstGt0 = filter ((>0).fst)
此定義既比原始定義更短,也更容易理解。我們可以清楚地看到它在做什麼:我們透過檢查某個東西是否大於零來過濾列表。我們在檢查什麼?fst 元素。
雖然轉換為無點風格通常會導致更清晰的程式碼,但這當然並不總是如此。例如,將以下對映轉換為無點風格會導致幾乎無法理解的內容:
foo = map (\x -> sqrt (3+4*(x^2))) foo = map (sqrt . (3+) . (4*) . (^2))
Prelude 中定義了一些組合子,它們對無點程式設計很有用:
uncurry接受一個型別為a -> b -> c的函式,並將其轉換為型別為(a,b) -> c的函式。這很有用,例如,當對映跨越一對列表時:
示例
Prelude> map (uncurry (*)) [(1,2),(3,4),(5,6)] [2,12,30]
curry是uncurry的反面,接受一個型別為(a,b) -> c的函式,並生成一個型別為a -> b -> c的函式。
flip反轉函式前兩個引數的順序。也就是說,它接受一個型別為a -> b -> c的函式,並生成一個型別為b -> a -> c的函式。例如,我們可以使用flip compare按反序對列表進行排序:
示例
Prelude> List.sortBy compare [5,1,8,3] [1,3,5,8] Prelude> List.sortBy (flip compare) [5,1,8,3] [8,5,3,1]
這與以下語句相同:
示例
Prelude> List.sortBy (\a b -> compare b a) [5,1,8,3] [8,5,3,1]
只是更短。
當然,並非所有函式都可以用無點風格編寫。例如:
square x = x*x
無法用無點風格編寫,除非使用其他組合子。例如,如果我們可以定義其他函式,我們可以寫:
pair x = (x,x) square = uncurry (*) . pair
但在這種情況下,這不是很有用。
| 練習 |
|---|
|
如果可能,將以下函式轉換為無點風格。 func1 x l = map (\y -> y*x) l
func2 f g l = filter f (map g l)
func3 f l = l ++ map f l
func4 l = map (\y -> y+2)
(filter (\z -> z `elem` [1..10])
(5:l))
func5 f l = foldr (\x y -> f (y,x)) 0 l
|
模式匹配
[edit | edit source]模式匹配是 Haskell(以及大多數函數語言程式設計語言)最強大的功能之一。它最常與case表示式結合使用,我們在關於 函式 的部分已經見過。讓我們回到關於 資料型別 的部分中的 Color 示例。我將重複我們在資料型別部分已經有的定義:
data Color
= Red
| Orange
| Yellow
| Green
| Blue
| Purple
| White
| Black
| Custom Int Int Int -- R G B components
deriving (Show,Eq)
然後,我們想要編寫一個函式,該函式將在 Color 型別的值與 Int 的三元組之間進行轉換,這三個 Int 分別對應於 RGB 值。具體來說,如果我們看到一個 Color 為 Red,我們想要返回 (255,0,0),因為這是紅色的 RGB 值。因此我們這樣寫(記住分段函式定義只是case語句):
colorToRGB Red = (255,0,0)
如果我們看到一個 Color 為 Orange,我們想要返回 (255,128,0);如果我們看到 Yellow,我們想要返回 (255,255,0),等等。最後,如果我們看到一個自定義顏色,它包含三個元件,我們想要用這些元件組成一個三元組,所以我們寫:
colorToRGB Orange = (255,128,0) colorToRGB Yellow = (255,255,0) colorToRGB Green = (0,255,0) colorToRGB Blue = (0,0,255) colorToRGB Purple = (255,0,255) colorToRGB White = (255,255,255) colorToRGB Black = (0,0,0) colorToRGB (Custom r g b) = (r,g,b)
然後,在我們的直譯器中,如果我們輸入:
示例
Color> colorToRGB Yellow (255,255,0)
發生的事情是這樣的:我們建立一個值,稱之為 ,它的值為 Yellow。然後我們將它應用於 colorToRGB。我們檢查是否可以將 與 Red 進行“匹配”。此匹配失敗,因為根據 Eq{Color} 的定義,Red 不等於 Yellow。我們繼續遍歷 colorToRGB 的定義,並嘗試將 Yellow 與 Orange 進行匹配。這也失敗了。然後我們嘗試將 Yellow 與 Yellow 進行匹配,這成功了,因此我們使用此函式定義,它只是返回值 (255,255,0),如預期。
假設相反,我們使用了一個自定義顏色:
示例
Color> colorToRGB (Custom 50 200 100) (50,200,100)
我們應用相同的匹配過程,從 Red 到 Black 的所有值都失敗了。然後我們嘗試將 Custom 50 200 100 與 Custom r g b 進行匹配。我們可以看到 Custom 部分匹配,所以我們繼續檢視子元素是否匹配。在匹配過程中,變數 r、g 和 b 本質上是萬用字元,因此將 r 與 50、g 與 200 和 b 與 100 進行匹配沒有任何問題。作為這種匹配的“副作用”,r 獲得值 50,g 獲得值 200,b 獲得值 100。因此,整個匹配成功,我們檢視此函式部分的定義,並使用 r、g 和 b 的匹配值捆綁三元組。
我們還可以編寫一個函式來檢查 Color 是否是自定義顏色:
isCustomColor (Custom _ _ _) = True isCustomColor _ = False
當我們將一個值應用於 isCustomColor 時,它會嘗試將該值與 Custom _ _ _ 進行匹配。如果該值為 Custom x y z(對於任何 x、y 和 z),此匹配將成功。_(下劃線)字元是一個“萬用字元”,將匹配任何內容,但不會執行將變數名放在那裡的繫結操作。如果此匹配成功,函式返回 True;但是,如果此匹配失敗,它會繼續執行下一行,該行將匹配任何內容,然後返回 False。
出於某種原因,我們可能想要定義一個函式,該函式告訴我們給定的顏色是否“明亮”,我的“明亮”定義是指它的 RGB 元件之一等於 255(公認是一個任意的定義,但這只是一個例子)。我們可以將此函式定義為:
isBright = isBright' . colorToRGB
where isBright' (255,_,_) = True
isBright' (_,255,_) = True
isBright' (_,_,255) = True
isBright' _ = False
讓我們細究一下此定義。isBright 函式是先前定義的函式 colorToRGB 和一個輔助函式 isBright' 的組合,該函式告訴我們給定的 RGB 值是否明亮。我們可以用 isBright c = isBright' (colorToRGB c) 來替換這裡的第一行,但這裡沒有必要顯式地編寫引數,所以我們沒有編寫。同樣,這種函式組合式程式設計需要一些時間來適應,因此在本教程中我會盡量經常使用它。
isBright' 輔助函式接受由 colorToRGB 生成的 RGB 三元組。它首先嚐試將其與 (255,_,_) 進行匹配,如果該值在第一個位置具有 255,則匹配成功。如果此匹配成功,isBright' 返回 True,isBright 也返回 True。第二行和第三行定義分別檢查三元組的第二個位置和第三個位置是否為 255。第四行,即穿透,匹配其他所有內容,並將其報告為不亮。
我們可能還想編寫一個函式來在 RGB 三元組和Color之間進行轉換。我們只需將所有內容放在一個Custom建構函式中,但這將違揹我們的目的;我們希望僅將Custom槽用於與預定義顏色不匹配的值。但是,我們不希望允許使用者構建像 (600,-40,99) 這樣的自定義顏色,因為這些是無效的 RGB 值。如果給出這樣的值,我們可以丟擲錯誤,但這可能難以處理。相反,我們使用Maybe資料型別。它在(Prelude 中)定義為
data Maybe a = Nothing
| Just a
我們的使用方法如下:我們的rgbToColor函式返回一個型別為Maybe Color的值。如果傳遞給我們的函式的 RGB 值無效,我們將返回Nothing,這對應於失敗。另一方面,如果 RGB 值有效,我們將建立相應的Color值並返回Just。執行此操作的程式碼如下
rgbToColor 255 0 0 = Just Red
rgbToColor 255 128 0 = Just Orange
rgbToColor 255 255 0 = Just Yellow
rgbToColor 0 255 0 = Just Green
rgbToColor 0 0 255 = Just Blue
rgbToColor 255 0 255 = Just Purple
rgbToColor 255 255 255 = Just White
rgbToColor 0 0 0 = Just Black
rgbToColor r g b =
if 0 <= r && r <= 255 &&
0 <= g && g <= 255 &&
0 <= b && b <= 255
then Just (Custom r g b)
else Nothing -- invalid RGB value
前八行將 RGB 引數與預定義值匹配,如果匹配,則rgbToColor返回Just相應的顏色。如果這些都不匹配,則rgbToColor的最後一個定義將第一個引數與r匹配,將第二個引數與g匹配,將第三個引數與b匹配(這會導致將這些值繫結作為副作用)。然後它檢查這些值是否有效(每個值都大於或等於零且小於或等於 255)。如果是,則返回Just (Custom r g b);否則,它返回Nothing,對應於無效的顏色。
利用這一點,我們可以編寫一個函式來檢查正確的 RGB 值是否有效
rgbIsValid r g b = rgbIsValid' (rgbToColor r g b)
where rgbIsValid' (Just _) = True
rgbIsValid' _ = False
在這裡,我們將輔助函式rgbIsValid'與我們的函式rgbToColor組合。輔助函式檢查rgbToColor返回的值是否為Just任何東西(萬用字元)。如果是,則返回True。否則,它匹配任何東西並返回False。
模式匹配不是魔法,雖然。你只能對資料型別進行匹配;你不能對函式進行匹配。例如,以下無效
f x = x + 1 g (f x) = x
即使g的預期含義很清楚(即,g x = x - 1),編譯器通常不知道f是否有逆函式,因此它無法執行這樣的匹配。
守衛
[edit | edit source]守衛可以被認為是模式匹配機制的擴充套件。它們允許你根據任意布林表示式來允許分段函式定義。守衛出現在函式的所有引數之後,但在等號之前,並以豎線開頭。我們可以使用守衛來編寫一個簡單的函式,該函式返回一個字串,告訴你比較兩個元素的結果
comparison x y | x < y = "The first is less"
| x > y = "The second is less"
| otherwise = "They are equal"
你可以將豎線讀作“使得”。因此,我們說comparison x y的值“使得”x 小於 y 為“第一個更小”。該值使得 x 大於 y 為“第二個更小”,而該值否則為“它們相等”。關鍵字otherwise僅定義為等於True,因此與任何透過該處的匹配項都匹配。因此,我們可以看到它有效
示例
Guards> comparison 5 10 "The first is less" Guards> comparison 10 5 "The second is less" Guards> comparison 7 7 "They are equal"
守衛與模式匹配結合使用。當一個模式匹配成功時,它會連續嘗試其所有守衛,直到一個守衛匹配成功。如果沒有任何守衛匹配成功,則模式匹配將繼續下一個模式。
守衛的一個好處是where子句對所有守衛都通用。因此,我們上一節中isBright函式的另一個可能的定義將是
isBright2 c | r == 255 = True
| g == 255 = True
| b == 255 = True
| otherwise = False
where (r,g,b) = colorToRGB c
該函式等效於先前版本,但執行其計算方式略有不同。它接受一個顏色c,並對其應用colorToRGB,生成一個 RGB 三元組,該三元組透過模式匹配匹配到(r,g,b)。此匹配成功,並將值r、g和b繫結到它們各自的值。第一個守衛檢查r是否為 255,如果是,則返回 true。第二個和第三個守衛分別檢查g和b是否為 255,如果匹配,則返回 true。最後一個守衛作為最後的手段觸發,並返回False。
例項宣告
[edit | edit source]為了宣告一個型別是某個類的例項,你需要為它提供一個例項宣告。大多數類提供所謂的“最小完整定義”。這意味著為了滿足類的定義,必須為該類實現的函式。在為你的型別編寫了這些函式後,你可以將它宣告為該類的例項。
TheEqClass
[edit | edit source]Eq 類有兩個成員(即兩個函式)
(==) :: Eq a => a -> a -> Bool (/=) :: Eq a => a -> a -> Bool
這些型別簽名中的第一個表示函式==是一個函式,它接受兩個a(它們是Eq的成員),並生成一個Bool。/=(不相等)的型別簽名是相同的。Eq 類的最小完整定義要求定義這兩個函式中的一個(如果你定義了==,則/=會透過否定==的結果自動定義,反之亦然)。這些宣告必須在例項宣告中提供。
這可以透過示例來最好地說明。假設我們有我們的顏色示例,這裡為了方便重複一遍
data Color
= Red
| Orange
| Yellow
| Green
| Blue
| Purple
| White
| Black
| Custom Int Int Int -- R G B components
我們可以透過以下宣告定義Color為Eq的例項
instance Eq Color where
Red == Red = True
Orange == Orange = True
Yellow == Yellow = True
Green == Green = True
Blue == Blue = True
Purple == Purple = True
White == White = True
Black == Black = True
(Custom r g b) == (Custom r' g' b') =
r == r' && g == g' && b == b'
_ == _ = False
這裡的第一行以關鍵字instance開始,告訴編譯器我們正在進行例項宣告。然後它指定了類Eq,以及將成為該類例項的型別Color。在那之後,是where關鍵字。最後是方法宣告。
方法宣告的前八行基本上是相同的。例如,第一個說表示式Red == Red的值等於True。第二行到第八行是相同的。自定義顏色的宣告略有不同。我們在==的兩邊模式匹配Custom。在左側,我們將r、g和b分別繫結到元件。在右側,我們將r'、g'和b'繫結到元件。然後我們說,這兩個自定義顏色只有在r == r'、g == g'和b == b'都相等時才相等。後續部分表示我們以前沒有宣告為相等的任何對都是不相等的。
TheShowClass
[edit | edit source]Show 類用於將任意值顯示為字串。該類有三個方法
show :: Show a => a -> String showsPrec :: Show a => Int -> a -> String -> String showList :: Show a => [a] -> String -> String
最小完整定義是show或showsPrec(我們將在後面討論showsPrec - 它存在是為了效率原因)。我們可以透過以下例項宣告定義我們的Color資料型別為Show的例項
instance Show Color where
show Red = "Red"
show Orange = "Orange"
show Yellow = "Yellow"
show Green = "Green"
show Blue = "Blue"
show Purple = "Purple"
show White = "White"
show Black = "Black"
show (Custom r g b) =
"Custom " ++ show r ++ " " ++
show g ++ " " ++ show b
此宣告精確地指定了如何將型別為Color的值轉換為String。同樣,前八行是相同的,只是簡單地接受一個Color並生成一個字串。處理自定義顏色的最後一行匹配出 RGB 元件,並透過將各個元件show的結果連線起來(中間用空格隔開,開頭用“Custom”)來建立一個字串。
其他重要類
[edit | edit source]還有一些其他重要的類,我將簡要提及,因為它們要麼被廣泛使用,要麼是因為我們很快就會使用它們。我不會提供示例例項宣告;現在你應該清楚如何做到這一點。
TheOrdClass
[edit | edit source]排序類,其函式為
compare :: Ord a => a -> a -> Ordering (<=) :: Ord a => a -> a -> Bool (>) :: Ord a => a -> a -> Bool (>=) :: Ord a => a -> a -> Bool (<) :: Ord a => a -> a -> Bool min :: Ord a => a -> a -> a max :: Ord a => a -> a -> a
幾乎任何函式都可以作為最小完整定義;但是,如果你只實現了一個函式,建議你實現compare。此函式返回一個型別為Ordering的值,它定義為
data Ordering = LT | EQ | GT
因此,例如,我們得到
示例
Prelude> compare 5 7 LT Prelude> compare 6 6 EQ Prelude> compare 7 5 GT
為了宣告一個型別為Ord的例項,你必須已經將它宣告為Eq的例項(換句話說,Ord是Eq的子類 - 有關更多資訊,請參閱關於類的部分)。
TheEnumClass
[edit | edit source]Enum 類用於列舉型別;也就是說,對於每個元素都有一個後繼者和一個前驅者的型別。它的方法是
pred :: Enum a => a -> a succ :: Enum a => a -> a toEnum :: Enum a => Int -> a fromEnum :: Enum a => a -> Int enumFrom :: Enum a => a -> [a] enumFromThen :: Enum a => a -> a -> [a] enumFromTo :: Enum a => a -> a -> [a] enumFromThenTo :: Enum a => a -> a -> a -> [a]
最小完整定義包含toEnum和fromEnum,它們用於在Int之間進行轉換。pred和succ函式分別給出前驅者和後繼者。enum函式列舉元素列表。例如,enumFrom x列出x之後的所有元素;enumFromThen x step列出從x開始以step大小的步長列出所有元素。To函式在給定元素處結束列舉。
TheNumClass
[edit | edit source]Num 類提供標準算術運算
(-) :: Num a => a -> a -> a (*) :: Num a => a -> a -> a (+) :: Num a => a -> a -> a negate :: Num a => a -> a signum :: Num a => a -> a abs :: Num a => a -> a fromInteger :: Num a => Integer -> a
除了negate,所有這些都顯而易見,negate是單目減運算子。也就是說,negate x 表示.
Read 類與 Show 類相反。它是一種將字串讀取為任意型別的值的方法。Read 的方法是
readsPrec :: Read a => Int -> String -> [(a, String)] readList :: String -> [([a], String)]
最小的完整定義是 readsPrec。與此相關最重要的函式是 read,它使用 readsPrec 作為
read s = fst (head (readsPrec 0 s))
如果解析字串失敗,這將失敗。您可以定義一個 maybeRead 函式為
maybeRead s =
case readsPrec 0 s of
[(a,_)] -> Just a
_ -> Nothing
如何在示例中進一步討論如何直接編寫和使用 readsPrec。
假設我們正在從頭開始定義 Maybe 資料型別。定義將類似於
data Maybe a = Nothing
| Just a
現在,當我們去寫例項宣告時,比如 Eq,我們需要知道 a 是 Eq 的例項,否則我們無法編寫宣告。我們將其表達為
instance Eq a => Eq (Maybe a) where
Nothing == Nothing = True
(Just x) == (Just x') = x == x'
這第一行可以讀作“a 是 Eq 的例項意味著 (=>) Maybe a 是 Eq 的例項”。
編寫像這樣的明顯的 Eq、Ord、Read 和 Show 類很乏味,應該自動化。幸運的是,它是。如果你編寫了一個“足夠簡單”的資料型別(除非你開始編寫不動點型別,否則你將編寫的幾乎所有資料型別都是如此),編譯器可以自動派生一些最基本類。要做到這一點,您只需新增一個deriving子句到資料型別宣告之後,如下所示
data Color
= Red
| ...
| Custom Int Int Int -- R G B components
deriving (Eq, Ord, Show, Read)
這將自動為 Color 資料型別建立命名類的例項。類似地,宣告
data Maybe a = Nothing
| Just a
deriving (Eq, Ord, Show, Read)
僅在 a 適當的時候派生這些類。
總而言之,您可以派生 Eq、Ord、Enum、Bounded、Show 和 Read 的例項。在“多型程式設計”或“泛型程式設計”領域做了大量工作,其中包括允許為任何類派生例項宣告。這遠遠超出了本教程的範圍;相反,我將您推薦給相關文獻。
我知道到目前為止,你可能已經厭倦了聽到關於資料型別的訊息。然而,它們非常重要,否則我不會花那麼多時間在它們身上。資料型別提供了一種符號上的便利,例如,如果你有一個數據型別包含許多值。這些被稱為命名欄位。
考慮一個數據型別,它的目的是儲存配置設定。通常,當你從這種型別中提取成員時,你實際上只關心許多設定中的一兩個。此外,如果許多設定具有相同的型別,你可能會經常發現自己在想:“等等,這是第四個還是第五個元素?”你可以做的一件事是編寫訪問器函式。考慮以下為終端程式製作的配置型別
data Configuration =
Configuration String -- user name
String -- local host
String -- remote host
Bool -- is guest?
Bool -- is super user?
String -- current directory
String -- home directory
Integer -- time connected
deriving (Eq, Show)
然後,你可以編寫訪問器函式,比如(我只列出了幾個)
getUserName (Configuration un _ _ _ _ _ _ _) = un getLocalHost (Configuration _ lh _ _ _ _ _ _) = lh getRemoteHost (Configuration _ _ rh _ _ _ _ _) = rh getIsGuest (Configuration _ _ _ ig _ _ _ _) = ig ...
你也可以編寫更新函式來更新單個元素。當然,現在如果你在配置中新增一個元素或刪除一個元素,所有這些函式現在都必須接受不同的引數數量。這非常煩人,並且很容易導致錯誤。但是,有一個解決方案。我們只需在資料型別宣告中為欄位命名,如下所示
data Configuration =
Configuration { username :: String,
localhost :: String,
remotehost :: String,
isguest :: Bool,
issuperuser :: Bool,
currentdir :: String,
homedir :: String,
timeconnected :: Integer
}
這將自動為我們生成以下訪問器函式
username :: Configuration -> String localhost :: Configuration -> String ...
此外,它還為我們提供了非常方便的更新方法。以下是一個針對“工作後目錄”和“更改目錄”的簡短示例,這些示例適用於 Configurations
changeDir :: Configuration -> String -> Configuration
changeDir cfg newDir =
-- make sure the directory exists
if directoryExists newDir
then -- change our current directory
cfg{currentdir = newDir}
else error "directory does not exist"
postWorkingDir :: Configuration -> String
-- retrieve our current directory
postWorkingDir cfg = currentdir cfg
因此,一般來說,要將資料型別 y 中的欄位 x 更新為 z,你編寫 y{x=z}。您可以更改多個欄位;每個欄位都應該用逗號隔開,例如,y{x=z, a=b, c=d}。
當然,您可以像以前一樣繼續對 Configuration 進行模式匹配。命名欄位只是語法糖;您仍然可以編寫類似於以下內容的內容
getUserName (Configuration un _ _ _ _ _ _ _) = un
但幾乎沒有理由這樣做。最後,您可以對命名欄位進行模式匹配,如下所示
getHostData (Configuration {localhost=lh,remotehost=rh})
= (lh,rh)
這將變數 lh 與 Configuration 上的 localhost 欄位匹配,並將變數 rh 與 Configuration 上的 remotehost 欄位匹配。當然,這些匹配會成功。您也可以透過在這些位置放置值而不是變數名來限制匹配,就像您對標準資料型別一樣。
您可以使用以下第一個定義中所示的舊方法建立 Configuration 的值,或者使用命名欄位的型別建立 Configuration 的值,如下面的第二個定義所示
initCFG =
Configuration "nobody" "nowhere" "nowhere"
False False "/" "/" 0
initCFG' =
Configuration
{ username="nobody",
localhost="nowhere",
remotehost="nowhere",
isguest=False,
issuperuser=False,
currentdir="/",
homedir="/",
timeconnected=0 }
儘管第二個可能更容易理解,除非你在程式碼中散佈了大量註釋。
待辦事項:在此處新增內容
回想一下,內建 Haskell 列表資料型別的定義等效於
data List a = Nil
| Cons a (List a)
除了 Nil 被稱為 [],Cons x xs 被稱為 x:xs 之外。這只是為了使模式匹配更容易,程式碼更小。讓我們研究一下如何編寫一些標準列表函式。考慮 map。以下給出了一個定義
map _ [] = [] map f (x:xs) = f x : map f xs
這裡,第一行表示當你對一個空列表進行 map 時,無論函式是什麼,你都會得到一個空列表。第二行表示當你對一個以 x 為頭,xs 為尾的列表進行 map 時,結果是將 f 應用於 x 並連線到 f 在 xs 上進行對映的結果。
filter 可以類似地定義
filter _ [] = []
filter p (x:xs) | p x = x : filter p xs
| otherwise = filter p xs
這應該很清楚。對於空列表,我們返回一個空列表。對於非空列表,我們返回尾部的過濾器,可能在前面加上頭部,具體取決於它是否滿足謂詞 p。
我們可以定義 foldr 為
foldr _ z [] = z foldr f z (x:xs) = f x (foldr f z xs)
這裡,最好的解釋是我們用一個特定值替換空列表 ([]),用某個函式替換列表構造器 (:)。在第一行,我們可以看到用 z 替換 []。使用反引號使 f 成為中綴運算子,我們可以將第二行寫成
foldr f z (x:xs) = x `f` (foldr f z xs)
由此,我們可以直接看到 : 如何被 f 替換。
最後,foldl
foldl _ z [] = z foldl f z (x:xs) = foldl f (f z x) xs
這稍微複雜一些。記住,z 可以被認為是當前狀態。因此,如果我們摺疊一個空列表,我們只需返回當前狀態。另一方面,如果列表不為空,它將是 x:xs 的形式。在這種情況下,我們透過將 f 應用於當前狀態 z 和當前列表元素 x 來獲得一個新狀態,然後對 xs 使用這個新狀態遞迴呼叫 foldl。
還有一類函式:zip 和 unzip 函式,它們分別接受多個列表並建立一個列表,或者接受一個列表並將它們拆分。例如,zip 執行以下操作
示例
Prelude> zip "hello" [1,2,3,4,5]
[('h',1),('e',2),('l',3),('l',4),('o',5)]
基本上,它將兩個列表的第一個元素配對,並將它們作為新列表的第一個元素。然後它將兩個列表的第二個元素配對,並將它們作為第二個元素,等等。如果列表長度不相等會怎樣?它會在較短的列表結束時停止。zip 的一個合理的定義是
zip [] _ = [] zip _ [] = [] zip (x:xs) (y:ys) = (x,y) : zip xs ys
unzip 函式則相反。它接受一個壓縮列表並返回兩個“原始”列表
示例
Prelude> unzip [('f',1),('o',2),('o',3)]
("foo",[1,2,3])
有一系列 zip 和 unzip 函式,名為 zip3、unzip3、zip4、unzip4 等等;...3 函式使用三元組而不是二元組;...4 函式使用四元組,等等。
最後,函式 `take` 獲取一個整數 和一個列表,並返回列表中的前 個元素。相應地,`drop` 獲取一個整數 和一個列表,並返回丟棄列表中前 個元素後的結果。這兩個函式都不會產生錯誤;如果 太大,它們都會返回較短的列表。
列表推導式
[edit | edit source]對於處理元素屬於 `Enum` 類(參見 例項 部分)的列表,比如 `Int` 或 `Char`,有一些語法糖。如果我們想要建立一個包含從 到 的所有元素的列表,我們可以簡單地寫
示例
Prelude> [1..10] [1,2,3,4,5,6,7,8,9,10]
我們也可以引入一個步長
示例
Prelude> [1,3..10] [1,3,5,7,9] Prelude> [1,4..10] [1,4,7,10]
這些表示式分別是 `enumFromTo` 和 `enumFromThenTo` 的簡寫形式。當然,您不需要指定上限。嘗試以下操作(但準備好按下 Control+C 停止計算!)
示例
Prelude> [1..]
[1,2,3,4,5,6,7,8,9,10,11,12{Interrupted!}
您可能列印了比這多出幾千個元素。正如我們之前所說,Haskell 是惰性的。這意味著從 1 開始的所有數字的列表都是完全有效的,這正是這個列表。當然,如果您嘗試列印列表(我們透過在直譯器中輸入它來隱式地執行此操作),它將不會停止。但是,如果我們只評估這個列表的初始段,我們就可以了
示例
Prelude> take 3 [1..] [1,2,3] Prelude> take 3 (drop 5 [1..]) [6,7,8]
這很有用,比如,如果我們想為列表中的每個元素分配一個 ID。如果沒有惰性,我們將不得不寫類似這樣的程式碼
assignID :: [a] -> [(a,Int)] assignID l = zip l [1..length l]
這意味著列表將被遍歷兩次。但是,由於惰性,我們可以簡單地寫
assignID l = zip l [1..]
我們就會得到我們想要的結果。我們可以看到這是有效的
示例
Prelude> assignID "hello"
[('h',1),('e',2),('l',3),('l',4),('o',5)]
最後,`map` 和 `filter` 有一些有用的語法糖,基於數學中標準的集合符號。在數學中,我們將寫類似 來表示當應用於 `s` 的元素時 `f` 的所有值的集合,這些元素滿足 `p`。這等效於 Haskell 語句 `map f (filter p s)`。但是,我們也可以使用更像數學的符號並寫 `[f x | x <- s, p x]`。雖然在數學中,管道右側語句的順序是自由的,但在 Haskell 中並非如此。我們不能將 `p x` 放在 `x <- s` 之前,否則編譯器將不知道 `x` 是什麼。我們可以用它來進行簡單的字串處理。假設我們想要取一個字串,只保留大寫字母,並將這些大寫字母轉換為小寫。我們可以透過以下兩種等效的方式中的任何一種來完成此操作
示例
Prelude> map toLower (filter isUpper "Hello World") "hw" Prelude> [toLower x | x <- "Hello World", isUpper x] "hw"
這兩個是等效的,並且,根據您使用的確切函式,其中一個可能比另一個更具可讀性。但是,我們還可以做更多的事情。假設您想要建立一個包含每對點的列表,這些點位於 (0,0) 和 (5,7) 之間,且低於對角線。使用列表和對映手動執行此操作會很麻煩,並且可能難以閱讀。使用列表推導式再簡單不過了
示例
Prelude> [(x,y) | x <- [1..5], y <- [x..7]] [(1,1),(1,2),(1,3),(1,4),(1,5),(1,6),(1,7),(2,2),(2,3), (2,4),(2,5),(2,6),(2,7),(3,3),(3,4),(3,5),(3,6),(3,7), (4,4),(4,5),(4,6),(4,7),(5,5),(5,6),(5,7)]
如果您顛倒了 `x <-` 和 `y <-` 子句的順序,遍歷空間的順序將被顛倒(當然,在這種情況下,`y` 將不再依賴於 `x`,您將需要使 `x` 依賴於 `y`,但這很簡單)。
陣列
[edit | edit source]列表在很多方面都很好用。很容易在它們開頭新增元素,並以各種改變列表長度的方式操作它們。但是,它們不適合隨機訪問,它們的平均複雜度為 以訪問任意元素(如果您不知道 的含義,您可以忽略它或快速繞道閱讀附錄章節 Complexity,這是一個介紹複雜性理論的兩頁內容)。因此,如果您願意放棄快速插入和刪除,因為您需要隨機訪問,那麼您應該使用陣列而不是列表。
為了使用陣列,您必須匯入 `Array` 模組。建立陣列有幾種方法,`array` 函式、`listArray` 函式和 `accumArray` 函式。`array` 函式獲取一個表示陣列邊界的對,以及一個表示陣列初始值的關聯列表。`listArray` 函式獲取邊界,然後只獲取一個值的列表。最後,`accumArray` 函式獲取一個累加函式、一個初始值和一個關聯列表,並將列表中的對累加到陣列中。以下是一些建立陣列的示例
示例
Prelude> :m Array Prelude Array> array (1,5) [(i,2*i) | i <- [1..5]] array (1,5) [(1,2),(2,4),(3,6),(4,8),(5,10)] Prelude Array> listArray (1,5) [3,7,5,1,10] array (1,5) [(1,3),(2,7),(3,5),(4,1),(5,10)] Prelude Array> accumArray (+) 2 (1,5) [(i,i) | i <- [1..5]] array (1,5) [(1,3),(2,4),(3,5),(4,6),(5,7)]
當陣列被打印出來(透過 show 函式)時,它們會以關聯列表的形式列印。例如,在第一個例子中,關聯列表顯示陣列在 處的值為 ,陣列在 處的值為 ,等等。
您可以使用 `!` 函式提取陣列中的元素,該函式接受一個數組和一個索引,例如
示例
Prelude Array> (listArray (1,5) [3,7,5,1,10]) ! 3 5
此外,您可以使用 `//` 函式更新陣列中的元素。該函式接受一個數組和一個關聯列表,並更新列表中指定的陣列位置
示例
Prelude Array> (listArray (1,5) [3,7,5,1,10]) // [(2,99),(3,-99)] array (1,5) [(1,3),(2,99),(3,-99),(4,1),(5,10)]
還有一些其他有趣的函式
bounds |
返回陣列的邊界 |
indices |
返回陣列所有索引的列表 |
elems |
按順序返回陣列中所有值的列表 |
assocs |
返回陣列的關聯列表 |
如果我們將 `arr` 定義為 `listArray (1,5) [3,7,5,1,10]`,則這些函式應用於 `arr` 的結果是
示例
Prelude Array> bounds arr (1,5) Prelude Array> indices arr [1,2,3,4,5] Prelude Array> elems arr [3,7,5,1,10] Prelude Array> assocs arr [(1,3),(2,7),(3,5),(4,1),(5,10)]
請注意,雖然陣列的訪問時間是 ,但它們並非 更新。實際上,它們是 更新,因為為了保持純度,必須 *複製* 陣列才能進行更新。因此,函式式陣列在您只填充一次並隨後只進行讀取時才真正有用。如果您需要快速訪問和更新,您可能應該使用 `FiniteMap`,它們將在有關 有限對映 的部分中討論,並且具有 的訪問和更新速度。
對映
[edit | edit source]本節內容正在編寫中,歡迎您幫助改進! |
來自 `Data.Map` 模組的 `Map` 資料型別是平衡樹的純函式式實現。對映在執行固定大小為 的這些資料型別上的各種操作所需的時間方面,可以與列表和陣列進行比較。簡要比較如下所示:
| List | Array | Map | |
|---|---|---|---|
| insert | |||
| update | |||
| delete | |||
| find | |||
| map |
我們可以看到,列表提供了快速插入(但其他操作都很慢),陣列提供了快速查詢(但其他操作都很慢),而對映則提供了一般速度的操作。
對映的型別是 `Map k a`,其中 `k` 是鍵的型別,`a` 是元素的型別。也就是說,對映是從型別 `k` 到型別 `a` 的查詢表。
基本對映函式如下所示:
empty :: Map k a insert :: k -> a -> Map k a -> Map k a delete :: k -> Map k a -> Map k a member :: k -> Map k a -> Bool lookup :: k -> Map k a -> a
在所有這些情況下,型別 `k` 必須是 `Ord` 的例項(因此也是 `Eq` 的例項)。
還存在 `fromList` 和 `toList` 函式,用於將列表轉換為對映和從對映轉換為列表。嘗試以下操作:
示例
Prelude> :m Data.Map
Prelude Data.Map> let mymap = fromList [('a',5),('b',10),('c',1),('d',2)]
Prelude Data.Map> let othermap = insert 'e' 6 mymap
Prelude Data.Map> toList mymap
[('a',5),('b',10),('c',1),('d',2)]
Prelude Data.Map> toList othermap
[('a',5),('b',10),('c',1),('d',2),('e',6)]
Prelude Data.Map> Data.Map.lookup 'e' othermap
6
Prelude Data.Map> Data.Map.lookup 'e' mymap
*** Exception: user error (Data.Map.lookup: Key not found)
佈局
[edit | edit source]關於列表的最後的話
[edit | edit source]您可能已經厭倦了聽到有關列表的資訊,但它們對於 Haskell(以及實際上所有函數語言程式設計)是如此基礎,如果我們不再談論它們,那將是非常糟糕的。
事實證明,foldr 實際上是一個非常強大的函式:它可以計算原始遞迴函式。原始遞迴函式本質上是隻能使用“for”迴圈,而不能使用“while”迴圈計算的函式。
事實上,我們可以相當容易地用foldr定義map
map2 f = foldr (\a b -> f a : b) []
這裡,b 是累加器(即結果列表),a 是當前正在考慮的元素。事實上,我們可以透過一系列步驟簡化這個定義
foldr (\a b -> f a : b) [] ==> foldr (\a b -> (:) (f a) b) [] ==> foldr (\a -> (:) (f a)) [] ==> foldr (\a -> ((:) . f) a) [] ==> foldr ((:) . f) []
這與foldr (:) [] 是列表上的恆等函式直接相關。這是因為,正如之前提到的,foldr f z 可以被認為是將列表中的[] 替換為z,將: 替換為f。在這種情況下,我們保持兩者不變,所以它是恆等函式。
事實上,你可以將任何以下風格的函式轉換為foldr
myfunc [] = z myfunc (x:xs) = f x (myfunc xs)
透過將最後一行用中綴形式的f 寫出來,這應該是顯而易見的
myfunc [] = z myfunc (x:xs) = x `f` (myfunc xs)
很明顯,我們只是將[] 替換為z,將: 替換為f。考慮filter 函式
filter p [] = []
filter p (x:xs) =
if p x
then x : filter p xs
else filter p xs
這個函式也遵循上面的形式。根據第一行,我們可以得出z 應該為[],就像map 的情況一樣。現在,假設我們將呼叫filter p xs 的結果簡稱為b,那麼我們可以將其改寫為
filter p [] = [] filter p (x:xs) = if p x then x : b else b
鑑於此,我們可以將filter 轉換為一個 fold
filter p = foldr (\a b -> if p a then a:b else b) []
讓我們考慮一個稍微複雜一點的函式:++。++ 的定義是
(++) [] ys = ys (++) (x:xs) ys = x : (xs ++ ys)
現在,問題是我們是否可以用 fold 表示法來寫它。首先,我們可以對第一行應用 eta 歸約,得到
(++) [] = id
透過一系列步驟,我們也可以對第二行進行 eta 歸約
(++) (x:xs) ys = x : ((++) xs ys) ==> (++) (x:xs) ys = (x:) ((++) xs ys) ==> (++) (x:xs) ys = ((x:) . (++) xs) ys ==> (++) (x:xs) = (x:) . (++) xs
因此,我們得到++ 的 eta 歸約定義為
(++) [] = id (++) (x:xs) = (x:) . (++) xs
現在,我們可以嘗試將它放入 fold 表示法中。首先,我們注意到基本情況將[] 轉換為id。現在,如果我們假設(++) xs 被稱為b,x 被稱為a,我們可以得到以下關於foldr 的定義
(++) = foldr (\a b -> (a:) . b) id
這在直覺上是有意義的。如果我們只考慮將++ 應用於一個引數,我們可以將其視為一個函式,它接受一個列表並建立一個函式,該函式在應用時會將該列表附加到另一個列表的前面。在 lambda 函式中,我們假設我們有一個函式b 會對列表的其餘部分執行此操作,我們需要建立一個函式,它會對b 以及單個元素a 執行此操作。為了做到這一點,我們首先應用b,然後將a 新增到前面。
我們可以透過以下步驟進一步將此表示式簡化為無點風格
==> (++) = foldr (\a b -> (a:) . b) id ==> (++) = foldr (\a b -> (.) (a:) b) id ==> (++) = foldr (\a -> (.) (a:)) id ==> (++) = foldr (\a -> (.) ((:) a)) id ==> (++) = foldr (\a -> ((.) . (:)) a) id ==> (++) = foldr ((.) . (:)) id
這個最終版本是無點的,雖然不一定容易理解。大概是原始版本更清晰。
作為最後的例子,考慮concat。我們可以將其寫成
concat [] = [] concat (x:xs) = x ++ concat xs
應該立即清楚,fold 的z 元素是[],遞迴函式是++,得到
concat = foldr (++) []
| 練習 |
|---|
|
