跳轉到內容

Haskell/函式進階

來自華夏公益教科書,自由的教科書

這裡介紹幾個讓函式使用起來更方便的特性。

重新認識 let 和 where

[編輯 | 編輯原始碼]

正如前面章節所述,letwhere 在區域性函式定義中很有用。這裡,sumStr 呼叫了 addStr 函式

addStr :: Float -> String -> Float
addStr x str = x + read str

sumStr :: [String] -> Float
sumStr = foldl addStr 0.0

但如果我們永遠不需要在其他地方使用 addStr 呢?那麼我們可以使用區域性繫結來重寫 sumStr。我們可以用let繫結...

sumStr =
   let addStr x str = x + read str
   in foldl addStr 0.0

... 或者用 where 語句...

sumStr = foldl addStr 0.0
   where addStr x str = x + read str

... 他們的區別似乎只是風格問題:我們更喜歡綁定出現在定義的其他部分之前還是之後?

然而,letwhere 之間還有另一個重要的區別。letlet...in結構是一個表示式,就像 if/then/else 一樣。相比之下,where 語句類似於守衛,因此不是表示式。因此,let 繫結可以用於複雜的表示式中

f x =
    if x > 0
        then (let lsq = (log x) ^ 2 in tan lsq) * sin x
        else 0

外層括號內的表示式是自包含的,並計算 x 對數平方的正切值。請注意,lsq 的作用域不超過括號,因此將 then 分支更改為

        then (let lsq = (log x) ^ 2 in tan lsq) * (sin x + lsq)

如果不刪除 let 周圍的括號,則無法正常工作。

儘管不是完整的表示式,where 語句可以被包含在 case 表示式中

describeColour c = 
   "This colour "
   ++ case c of
          Black -> "is black"
          White -> "is white"
          RGB red green blue -> " has an average of the components of " ++ show av
             where av = (red + green + blue) `div` 3
   ++ ", yeah?"

在這個例子中,where 語句的縮排設定了變數 av 的作用域,使其僅在where語句的作用域內有效,即只有在av變數所在的RGB red green bluecase 語句中有效。如果將 where 語句放在與 case 語句相同的縮排級別,則它將對所有 case 語句有效。以下是一個使用守衛的例子

doStuff :: Int -> String
doStuff x
  | x < 3     = report "less than three"
  | otherwise = report "normal"
  where
    report y = "the input is " ++ y

注意,由於每個守衛只有一個等號,因此我們無法在 let 表示式中放置一個作用域能夠覆蓋所有守衛的表示式,就像 where 語句一樣。因此,這種情況尤其適合使用 where 語句。

匿名函式 - lambda 表示式

[編輯 | 編輯原始碼]

為什麼為一個像 addStr 這樣的函式建立正式的名稱,而它只存在於另一個函式的定義中,永遠不會被再次使用?相反,我們可以將其設為匿名函式,也稱為“lambda 函式”。然後,sumStr 可以這樣定義

sumStr = foldl (\ x str -> x + read str) 0.0

括號中的表示式是一個 lambda 函式。反斜槓用作希臘字母 lambda(λ)的最接近的 ASCII 等價物。這個 lambda 函式接受兩個引數 xstr,並計算為“x + read str”。因此,上面介紹的 sumStr 與使用 let 繫結中 addStrsumStr 完全相同。

lambda 表示式對於編寫與 map、fold 及其兄弟函式一起使用的臨時函式非常方便,尤其是在該函式很簡單的情況下(注意不要將複雜的表示式塞進 lambda 表示式中——這會降低可讀性)。

由於 lambda 表示式中綁定了變數(繫結到引數,就像在普通函式定義中一樣),因此模式匹配也可以在其中使用。一個簡單的例子是使用 lambda 表示式重新定義 tail

tail' = (\ (_:xs) -> xs)

注意:由於 lambda 表示式在 Haskell 中是一個特殊字元,因此 \ 本身將被視為函式,而緊隨其後的任何非空格字元將作為第一個引數的變數。最好在 lambda 和引數之間加上空格(尤其是在 lambda 接受多個引數時,這使得程式碼更易讀)。

運算子

[編輯 | 編輯原始碼]

在 Haskell 中,任何接受兩個引數並且名稱完全由非字母數字字元組成的函式都被認為是運算子。最常見的例子是算術運算子,如加法 (+) 和減法 (-)。與其他函式不同,運算子通常用中綴形式(寫在兩個引數之間)使用。所有運算子也可以用括號括起來,然後像其他函式一樣用字首形式使用

-- these are the same:
2 + 4
(+) 2 4

我們可以像其他函式一樣以常規方式定義新運算子——只是不要在它們的名稱中使用任何字母數字字元。例如,以下是從 Data.List 定義的集合差運算子

(\\) :: (Eq a) => [a] -> [a] -> [a]
xs \\ ys = foldl (\zs y -> delete y zs) xs ys

如上例所示,運算子也可以以中綴形式定義。以字首形式編寫的相同定義也適用

(\\) xs ys = foldl (\zs y -> delete y zs) xs ys

注意,運算子的型別宣告沒有中綴版本,必須用括號括起來。

是一個很巧妙的語法糖,可以用在運算子上。括號中的運算子,兩側都有其引數之一...

(2+) 4
(+4) 2

... 本身就是一個新的函式。例如,(2+) 的型別是 (Num a) => a -> a。我們可以將節傳遞給其他函式,例如 map (+2) [1..4] == [3..6]。再舉一個例子,我們可以為我們在列表 II 中編寫的 multiplyList 函式新增額外的修飾

multiplyList :: Integer -> [Integer] -> [Integer]
multiplyList m = map (m*)


如果你有一個“普通”的字首函式,想要將其用作運算子,只需用反引號將其括起來即可

1 `elem` [1..4]

這被稱為使函式中綴。通常這樣做是為了提高可讀性:1 `elem` [1..4]elem 1 [1..4] 閱讀起來更好。你也可以定義中綴函式

elem :: (Eq a) => a -> [a] -> Bool
x `elem` xs = any (==x) xs

但再次注意,型別簽名仍然保持字首風格。

節甚至可以與中綴函式一起使用

(1 `elem`) [1..4]
(`elem` [1..4]) 1

當然,請記住,你只能使二元函式(即接受兩個引數的函式)成為中綴函式。

練習
  • lambda 表示式是避免定義不必要單獨函式的一種好方法。將以下 let 或 where 繫結轉換為 lambda 表示式
    • map f xs where f x = x * 2 + 3
    • let f x y = read x + y in foldr f 1 xs
  • 節只是 lambda 操作的語法糖。即 (+2) 等價於 \x -> x + 2。以下節將“反糖化”成什麼?它們的型別是什麼?
    • (4+)
    • (1 `elem`)
    • (`notElem` "abc")
華夏公益教科書