# 0. 函数的语法
本章讲解 Haskell 中函数的语法,以及:
- 怎样快速地解析(deconstruct)输入的值
- 如何避免大坨的 if-else 链
- 如何保存中间结果便于复用
# 1. 模式匹配
模式匹配(pattern matching)通过检查数据的特定结构来检查是否匹配,并按模式从中解析数据。
在 Haskell 中定义函数时,可以为不同的模式分别定义函数体,这让代码更加简洁、易读。
```hs
lucky :: Int -> String
lucky 7 = "LUCKY NUMBER SEVEN!"
lucky x = "Sorry, you're out of luck, pal!"
```
模式中给出的非具体值是一个万能模式(catchall pattern)。
它总能匹配输入的参数,并将其绑定到模式中的名字供引用。
使用模式匹配可以避免 if-then-else 树。
```hs
sayMe :: Int -> String
sayMe 1 = "One!"
sayMe 2 = "Two!"
sayMe 3 = "Three!"
sayMe 4 = "Four!"
sayMe 5 = "Five!"
sayMe x = "Not between 1 and 5"
```
如果将最后的模式 `sayMe x` 挪到最前面,函数的结果将永远是 `"Not between 1 and 5"`。
因为该模式匹配所有数,它不给后面的模式留任何机会。
模式匹配可用来写递归函数。
```hs
factorial :: Int -> Int
factorial 0 = 1
factorial n = n * factorial (n - 1)
```
模式匹配可能会失败。
```hs
charName :: Char -> String
charName 'a' = "Albert"
charName 'b' = "Broseph"
charName 'c' = "Cecil"
```
拿一个上面的函数没有考虑到的字符去调用它会导致错误 `Non-exhaustive patterns`。
定义模式时要考虑全面,一定要留一个万能模式以防不可预料的输入导致崩溃。
## 1.1. 元组的模式匹配
对元组同样可以使用模式匹配。
编写一个计算二维空间中向量(以序对的形式表示)的和的函数。
```hs
addVectors :: (Double, Double) -> (Double, Double) -> (Double, Double)
addVectors a b = (fst a + fst b, snd a + snd b)
```
用上模式匹配就漂亮多了。
```hs
addVectors :: (Double, Double) -> (Double, Double) -> (Double, Double)
addVectors (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)
```
序对有 `fst` 函数和 `snd` 函数可以取出元素,可以为三元组实现类似的函数。
```hs
first :: (a, b, c) -> a
first (x, _, _) = x
second :: (a, b, c) -> b
second (_, y, _) = y
third :: (a, b, c) -> c
third (_, _, z) = z
```
这里的下划线 `_` 为泛变量(generi variable),用于占位并表示不关心这部分的具体内容。
## 1.2. 列表与列表推导式的模式匹配
在列表推导式中也可以使用模式匹配。
```ghci
ghci> let xs = [(1, 3), (4, 3), (2, 4), (5, 3), (5, 6), (3, 1)]
ghci> [a + b | (a, b) <- xs]
[4,7,6,8,11,4]
```
一旦匹配失败,它就简单挪到下一个元素,匹配失败的元素不会被包含在列表推导式的结果中。
对于普通的列表也可以使用模式匹配。
可以用 `[]` 来匹配空列表,也可以配合使用 `:` 和 `[]` 来匹配非空列表。
像 `x : xs` 这样的模式可以将列表的头部绑定为 `x`,尾部绑定为 `xs`。
如果列表只有一个元素,那么 `xs` 就是一个空列表。
`x : xs` 模式在 Haskell 中应用非常广泛,尤其是递归函数,不过它只能匹配非空列表。
实现自己的 `head` 函数:
```hs
head' :: [a] -> a
head' [] = error "Can't call head on an empty list, dummy!"
head' (x : _) = x
```
> 绑定多个变量必须用括号将其括起,否则 Haskell 有可能会无法正确解析这段代码。
> `error` 函数可以生成一个运行时错误,用参数中的字符串表示对错误的描述。
另一个例子:
```hs
tell :: Show a => [a] -> String
tell [] = "The list is empty"
tell (x : []) = "The list has one element: " ++ show x
tell (x : y : []) = "The list has two elements: " ++ show x ++ " and " ++ show y
tell (x : y : _) = "This list is long. The first two elements are: " ++ show x ++ " and " ++ show y
```
> 这里的 `(x : [])` 与 `(x : y : [])` 也可以写作 `[x]` 和 `[x, y]`。
最后要注意的一点是,不能在关于列表的模式匹配中使用 `++` 运算符。
## 1.3. as 模式
as 模式(as-pattern)是一种特殊的模式。
as 模式允许按模式把一个值分割成多个项,同时仍保留对其整体的引用。
使用 as 模式,只需将一个名字和符号 `@` 置于普通模式的前面即可。
```hs
firstletter :: String -> String
firstletter "" = "Empty string whoops!"
firstletter all @ (x : xs) = "The first letter of " ++ all ++ " is " ++ [x]
```
```ghci
ghci> firstletter "Dracula"
"The first letter of Dracula is D"
```
# 2. 注意,哨卫!
模式用来检查参数的结构是否匹配,哨卫(guard)则用来检查参数的性质是否为真。
先看一个用到哨卫的函数:
```hs
bmiTell :: Double -> String
bmiTell bmi
| bmi <= 18.5 = "You're underweight, you emo, you!"
| bmi <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"
| bmi <= 30.0 = "You're fat! Lose some weight, fatty!"
| otherwise = "You're a whale, congratulations!"
```
哨卫跟在竖线符号 `|` 的右边,一个哨卫就是一个布尔表达式,每条哨卫语句至少缩进一个空格。
如果计算为 `True` 就选择对应的函数体,否则开始对下一个哨卫求值,不断重复这一过程。
一般而言,排在最后的哨卫都是 `otherwise`,它能捕获一切条件。
如果一个函数的所有哨卫都没有通过且没有 `otherwise` 作为万能条件,就转入下一个模式。
如果始终没有找到合适的哨卫或模式,就会导致一个错误。
哨卫可以在多个参数的函数中使用。
```hs
bmiTell :: Double -> Double -> String
bmiTell weight height
| weight / height ^ 2 <= 18.5 = "You're underweight, you emo, you!"
| weight / height ^ 2 <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"
| weight / height ^ 2 <= 30.0 = "You're fat! Lose some weight, fatty!"
| otherwise = "You're a whale, congratulations!"
```
实现自己的 `max` 函数:
```hs
max' :: Ord a => a -> a -> a
max' a b
| a < b = b
| otherwise = a
```
实现自己的 `compare` 函数:
```hs
myCompare :: Ord a => a -> a -> Ordering
a `myCompare` b
| a == b = EQ
| a <= b = LT
| otherwise = GT
```
> 通过反单引号 `` ` ``,不仅能以中缀形式调用函数,还可以直接按中缀形式定义函数。
# 3. `where` ?!
在命令式语言中,可以将计算的结果保存到一个变量中。
Haskell 中可借助 `where` 关键字保存计算的中间结果,便于复用。
```hs
bmiTell :: Double -> Double -> String
bmiTell weight height
| bmi <= 18.5 = "You're underweight, you emo, you!"
| bmi <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"
| bmi <= 30.0 = "You're fat! Lose some weight, fatty!"
| otherwise = "You're a whale, congratulations!"
where bmi = weight / height ^ 2
```
```hs
bmiTell :: Double -> Double -> String
bmiTell weight height
| bmi <= skinny = "You're underweight, you emo, you!"
| bmi <= normal = "You're supposedly normal. Pffft, I bet you're ugly!"
| bmi <= fat = "You're fat! Lose some weight, fatty!"
| otherwise = "You're a whale, congratulations!"
where bmi = weight / height ^ 2
skinny = 18.5
normal = 25.0
fat = 30.0
```
> 使用 `where` 关键字时变量定义都必须对齐于同一列。
如果不这样规范,Haskell 就会不清楚它们各自位于哪个代码块。
## 3.1. `where` 的作用域
`where` 块中定义的名字只对本函数的本模式可见,不会污染其他模式或函数的命名空间。
若想在不同的模式或函数中重复用到同一名字,应该把它置于全局定义中。
```hs
badGreeting :: String
badGreeting = "Oh! Pfft. It's you."
niceGreeting :: String
niceGreeting = "Hello! So very nice to see you,"
greet :: String -> String
greet "Juan" = niceGreeting ++ " Juan!"
greet "Fernando" = niceGreeting ++ " Fernando!"
greet name = badGreeting ++ " " ++ name
```
## 3.2. `where` 中的模式匹配
`where` 绑定中可以使用模式匹配。
```hs
bmiTell :: Double -> Double -> String
bmiTell weight height
| bmi <= skinny = "You're underweight, you emo, you!"
| bmi <= normal = "You're supposedly normal. Pffft, I bet you're ugly!"
| bmi <= fat = "You're fat! Lose some weight, fatty!"
| otherwise = "You're a whale, congratulations!"
where bmi = weight / height ^ 2
(skinny, normal, fat) = (18.5, 25.0, 30.0)
```
```hs
initials :: String -> String -> String
initials firstname lastname = [f] ++ ". " ++ [l] ++ "."
where (f : _) = firstname
(l : _) = lastname
```
## 3.3. `where` 块中的函数
`where` 块中可以定义函数。
```hs
calcBmis :: [(Double, Double)] -> [Double]
calcBmis xs = [bmi w h | (w, h) <- xs]
where bmi weight height = weight / height ^ 2
```
# 4. `let`
`let` 表达式允许在任何位置定义局部变量,且对其他哨卫不可见。
如 Haskell 中所有赋值结构一样,`let` 表达式可以使用模式匹配。
`let` 表达式的格式为 `let <bindings> in <expressions>`。
在 `let` 中绑定的名字仅对 `in` 部分可见。
```hs
cylinder :: Double -> Double -> Double
cylinder r h =
let sideArea = 2 * pi * r * h
topArea = pi * r * 2
in sideArea + 2 * topArea
```
`let` 表达式和 `where` 绑定的不同在于它是个表达式,可以用于任何位置。
使用 `let` 表达式时若需要在一行中绑定多个名字可以用分号将其分开。
```ghci
ghci> [let square x = x * x in (square 5, square 3, square 2)]
[(25,9,4)]
ghci> (let a = 100; b = 200; c = 300 in a * b * c, let foo = "Hey "; bar = "there!" in foo ++ bar)
(6000000,"Hey there!")
ghci> (let (a, b, c) = (1, 2, 3) in a + b + c) * 100
600
```
## 4.1. 列表推导式中的 `let`
在列表推导式中使用 `let` 表达式时与使用谓词差不多。
不同的是 `let` 表达式做的不是过滤,而是绑定名字。
绑定的名字只在列表推导式的输出部分和绑定后的表达式中可见。
```hs
calcBmis :: [(Double, Double)] -> [Double]
calcBmis xs = [bmi | (w, h) <- xs, let bmi = w / h ^ 2, bmi > 25.0]
```
> `(w, h) <- xs` 部分被称为生成器(generator)。
## 4.2. GHCi 中的 `let`
直接在 GHCi 中定义函数和常量时,`let` 的 `in` 部分可以省略。
如果省略 `in` 部分,`let` 中定义的名字将在整个会话过程中可见。
带 `in` 部分的 `let` 是一个有返回值的表达式,GHCi 不会记忆其中定义的名字。
```ghci
ghci> let zoot x y z = x * y + z
ghci> zoot 3 9 2
29
ghci> let boot x y z = x * y + z in boot 3 4 2
14
```
# 5. `case` 表达式
使用 `case` 表达式可以对变量的不同情况分别求值,可以使用模式匹配。
`case` 表达式与函数定义中对参数的模式匹配十分相似。
实际上函数定义的模式匹配本质上就是 `case` 表达式的语法糖。
下面两段代码是完全等价的:
```hs
head' :: [a] -> a
head' [] = error "No head for empty lists!"
head' (x : _) = x
```
```hs
head' :: [a] -> a
head' xs = case xs of [] -> error "No head for empty lists!"
(x : _) -> x
```
`case` 表达式的语法结构如下:
```
case <expression> of <pattern> -> <result>
<pattern> -> <result>
<pattern> -> <result>
...
```
`case` 表达式和函数定义的模式匹配不同在于它是个表达式,可以用于任何位置。
```hs
describeList :: [a] -> String
describeList ls = "The list is " ++ case ls of [] -> "empty."
[x] -> "a singleton list."
xs -> "a longer list."
```

《Haskell 趣学指南》学习笔记 - 第 3 章