本章讲的就是 Haskell 那套独特的语法结构,先从模式匹配开始。模式匹配经过检查数据的特定结构来检查其是否匹配,并按模式从中取得数据。html
在定义函数时,你能够为不一样的模式分别定义函数自己,这就让代码更加简洁易读。你能够匹配一切数据类型 --- 数字,字符,List,元组,等等。咱们弄个简单函数,让它检查咱们传给它的数字是否是 7。程序员
lucky :: (Integral a) => a -> String
lucky 7 = "LUCKY NUMBER SEVEN!"
lucky x = "Sorry, you're out of luck, pal!"
在调用 lucky
时,模式会从上至下进行检查,一旦有匹配,那对应的函数体就被应用了。这个模式中的惟一匹配是参数为 7,若是不是 7,就转到下一个模式,它匹配一切数值并将其绑定为x
。这个函数彻底可使用 if
实现,不过咱们若要个分辨 1 到 5 中的数字,而无视其它数的函数该怎么办?要是没有模式匹配的话,那可得好大一棵 if-else
树了!express
sayMe :: (Integral a) => a -> String
sayMe 1 = "One!"
sayMe 2 = "Two!"
sayMe 3 = "Three!"
sayMe 4 = "Four!"
sayMe 5 = "Five!"
sayMe x = "Not between 1 and 5"
注意下,若是咱们把最后匹配一切的那个模式挪到最前,它的结果就全都是 "Not between 1 and 5"
了。由于它本身匹配了一切数字,不给后面的模式留机会。编程
记得前面实现的那个阶乘函数么?当时是把 n
的阶乘定义成了 product [1..n]
。也能够写出像数学那样的递归实现,先说明 0 的阶乘是 1 ,再说明每一个正整数的阶乘都是这个数与它前驱 (predecessor) 对应的阶乘的积。以下即是翻译到 Haskell 的样子:api
factorial :: (Integral a) => a -> a
factorial 0 = 1
factorial n = n * factorial (n - 1)
这就是咱们定义的第一个递归函数。递归在 Haskell 中十分重要,咱们会在后面深刻理解。若是拿一个数(如 3)调用factorial
函数,这就是接下来的计算步骤:先计算 3*factorial 2
,factorial 2
等于 2*factorial 1
,也就是 3*(2*(factorial 1))
。factorial 1
等于 1*factorial 0
,好,得 3*(2*(1*factorial 0))
,递归在这里到头了,嗯 --- 咱们在万能匹配前面有定义,0 的阶乘是 1.因而最终的结果等于 3*(2*(1*1))
。如果把第二个模式放在前面,它就会捕获包括 0 在内的一切数字,这一来咱们的计算就永远都不会中止了。这即是为何说模式的顺序是如此重要:它老是优先匹配最符合的那个,最后才是那个万能的。安全
模式匹配也会失败。假如这个函数:编程语言
charName :: Char -> String
charName 'a' = "Albert"
charName 'b' = "Broseph"
charName 'c' = "Cecil"
拿个它没有考虑到的字符去调用它,你就会看到这个:ide
ghci> charName 'a'
"Albert"
ghci> charName 'b'
"Broseph"
ghci> charName 'h'
"*** Exception: tut.hs:(53,0)-(55,21): Non-exhaustive patterns in function charName
它告诉咱们说,这个模式不够全面。所以,在定义模式时,必定要留一个万能匹配的模式,这样咱们的进程就不会为了避免可预料的输入而崩溃了。函数
对 Tuple 一样可使用模式匹配。写个函数,将二维空间中的矢量相加该如何?将它们的 x
项和 y
项分别相加就是了。若是不了解模式匹配,咱们极可能会写出这样的代码:oop
addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a)
addVectors a b = (fst a + fst b, snd a + snd b)
嗯,能够运行。但有更好的方法,上模式匹配:
addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a)
addVectors (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)
there we go!好多了!注意,它已是个万能的匹配了。两个 addVector
的类型都是addVectors:: (Num a) => (a,a) -> (a,a) -> (a,a)
,咱们就可以保证,两个参数都是序对 (Pair) 了。
fst
和 snd
能够从序对中取出元素。三元组 (Tripple) 呢?嗯,没现成的函数,得本身动手:
first :: (a, b, c) -> a
first (x, _, _) = x
second :: (a, b, c) -> b
second (_, y, _) = y
third :: (a, b, c) -> c
third (_, _, z) = z
这里的 _
就和 List Comprehension 中同样。表示咱们不关心这部分的具体内容。
说到 List Comprehension,我想起来在 List Comprehension 中也能用模式匹配:
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]
一旦模式匹配失败,它就简单挪到下个元素。
对 List 自己也可使用模式匹配。你能够用 []
或 :
来匹配它。由于 [1,2,3]
本质就是 1:2:3:[]
的语法糖。你也可使用前一种形式,像 x:xs
这样的模式能够将 List 的头部绑定为 x
,尾部绑定为 xs
。若是这 List 只有一个元素,那么 xs
就是一个空 List。
Note:
x:xs
这模式的应用很是普遍,尤为是递归函数。不过它只能匹配长度大于等于 1 的 List。
若是你要把 List 的前三个元素都绑定到变量中,可使用相似 x:y:z:xs
这样的形式。它只能匹配长度大于等于 3 的 List。
咱们已经知道了对 List 作模式匹配的方法,就实现个咱们本身的 head
函数。
head' :: [a] -> a
head' [] = error "Can't call head on an empty list, dummy!"
head' (x:_) = x
看看管无论用:
ghci> head' [4,5,6]
4
ghci> head' "Hello"
'H'
漂亮!注意下,你若要绑定多个变量(用 _
也是如此),咱们必须用括号将其括起。同时注意下咱们用的这个 error
函数,它能够生成一个运行时错误,用参数中的字串表示对错误的描述。它会直接致使进程崩溃,所以应谨慎使用。但是对一个空 List 取head
真的不靠谱哇。
弄个简单函数,让它用非标准的英语给咱们展现 List 的前几项。
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
这个函数顾及了空 List,单元素 List,双元素 List 以及较长的 List,因此这个函数很安全。(x:[])
与 (x:y:[])
也能够写做 [x]
和 [x,y]
(有了语法糖,咱们没必要多加括号)。不过 (x:y:_)
这样的模式就不行了,由于它匹配的 List 长度不固定。
咱们曾用 List Comprehension 实现过本身的 length
函数,如今用模式匹配和递归从新实现它:
length' :: (Num b) => [a] -> b
length' [] = 0
length' (_:xs) = 1 + length' xs
这与先前写的那个 factorial
函数很类似。先定义好未知输入的结果 --- 空 List,这也叫做边界条件。再在第二个模式中将这 List 分割为头部和尾部。说,List 的长度就是其尾部的长度加 1。匹配头部用的 _
,由于咱们并不关心它的值。同时也应明确,咱们顾及了 List 全部可能的模式:第一个模式匹配空 List,第二个匹配任意的非空 List。
看下拿 "ham"
调用 length'
会怎样。首先它会检查它是否为空 List。显然不是,因而进入下一模式。它匹配了第二个模式,把它分割为头部和尾部并没有视掉头部的值,得长度就是 1+length' "am"
。ok。以此类推,"am"
的 length
就是1+length' "m"
。好,如今咱们有了 1+(1+length' "m")
。length' "m"
即 1+length ""
(也就是1+length' []
)。根据定义,length' []
等于 0
。最后得 1+(1+(1+0))
。
再实现 sum
。咱们知道空 List 的和是 0,就把它定义为一个模式。咱们也知道一个 List 的和就是头部加上尾部的和的和。写下来就成了:
sum' :: (Num a) => [a] -> a
sum' [] = 0
sum' (x:xs) = x + sum' xs
还有个东西叫作 as
模式,就是将一个名字和 @
置于模式前,能够在按模式分割什么东西时仍保留对其总体的引用。如这个模式 xs@(x:y:ys)
,它会匹配出与 x:y:ys
对应的东西,同时你也能够方便地经过 xs
获得整个 List,而没必要在函数体中重复 x:y:ys
。看下这个 quick and dirty 的例子:
capital :: String -> String
capital "" = "Empty string, whoops!"
capital all@(x:xs) = "The first letter of " ++ all ++ " is " ++ [x]
ghci> capital "Dracula"
"The first letter of Dracula is D"
咱们使用 as
模式一般就是为了在较大的模式中保留对总体的引用,从而减小重复性的工做。
还有——你不能够在模式匹配中使用 ++
。如有个模式是 (xs++ys)
,那么这个 List 该从什么地方分开呢?不靠谱吧。而(xs++[x,y,z])
或只一个 (xs++[x])
或许还能说的过去,不过出于 List 的本质,这样写也是不能够的。
模式用来检查一个值是否合适并从中取值,而 guard 则用来检查一个值的某项属性是否为真。咋一听有点像是 if
语句,实际上也正是如此。不过处理多个条件分支时 guard 的可读性要高些,而且与模式匹配契合的很好。
在讲解它的语法前,咱们先看一个用到 guard 的函数。它会依据你的 BMI 值 (body mass index,身体质量指数)来不一样程度地侮辱你。BMI 值即为体重除以身高的平方。若是小于 18.5,就是太瘦;若是在 18.5 到 25 之间,就是正常;25 到 30 之间,超重;若是超过 30,肥胖。这就是那个函数(咱们目前暂不为您计算 bmi,它只是直接取一个 emi 值)。
bmiTell :: (RealFloat a) => a -> 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!"
guard 由跟在函数名及参数后面的竖线标志,一般他们都是靠右一个缩进排成一列。一个 guard 就是一个布尔表达式,若是为真,就使用其对应的函数体。若是为假,就送去见下一个 guard,如之继续。若是咱们用 24.3 调用这个函数,它就会先检查它是否小于等于 18.5,显然不是,因而见下一个 guard。24.3 小于 25.0,所以经过了第二个 guard 的检查,就返回第二个字串。
在这里则是至关的简洁,不过不难想象这在命令式语言中又会是怎样的一棵 if-else 树。因为 if-else 的大树比较杂乱,如果出现问题会很难发现,guard 对此则十分清楚。
最后的那个 guard 每每都是 otherwise
,它的定义就是简单一个 otherwise = True
,捕获一切。这与模式很相像,只是模式检查的是匹配,而它们检查的是布尔表达式 。若是一个函数的全部 guard 都没有经过(并且没有提供 otherwise
做万能匹配),就转入下一模式。这即是 guard 与模式契合的地方。若是始终没有找到合适的 guard 或模式,就会发生一个错误。
固然,guard 能够在含有任意数量参数的函数中使用。免得用户在使用这函数以前每次都本身计算 bmi
。咱们修改下这个函数,让它取身高体重为咱们计算。
bmiTell :: (RealFloat a) => a -> a -> 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!"
你能够测试本身胖不胖。
ghci> bmiTell 85 1.90
"You're supposedly normal. Pffft, I bet you're ugly!"
运行的结果是我不太胖。不过程序却说我很丑。
要注意一点,函数的名字和参数的后面并无 =
。许多初学者会形成语法错误,就是由于在后面加上了 =
。
另外一个简单的例子:写个本身的 max
函数。应该还记得,它是取两个可比较的值,返回较大的那个。
max' :: (Ord a) => a -> a -> a
max' a b
| a > b = a
| otherwise = b
guard 也能够塞在一行里面。但这样会丧失可读性,所以是不被鼓励的。即便是较短的函数也是如此,不过出于展现,咱们能够这样重写 max'
:
max' :: (Ord a) => a -> a -> a
max' a b | a > b = a | otherwise = b
这样的写法根本一点都不容易读。
咱们再来试试用 guard 实现咱们本身的 compare
函数:
myCompare :: (Ord a) => a -> a -> Ordering
a `myCompare` b
| a > b = GT
| a == b = EQ
| otherwise = LT
ghci> 3 `myCompare` 2
GT
Note:经过反单引号,咱们不只能够以中缀形式调用函数,也能够在定义函数的时候使用它。有时这样会更易读。
前一节中咱们写了这个 bmi
计算函数:
bmiTell :: (RealFloat a) => a -> a -> 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!"
注意,咱们重复了 3 次。咱们重复了 3 次。程序员的字典里不该该有"重复"这个词。既然发现有重复,那么给它一个名字来代替这三个表达式会更好些。嗯,咱们能够这样修改:
bmiTell :: (RealFloat a) => a -> a -> 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
咱们的 where
关键字跟在 guard 后面(最好是与竖线缩进一致),能够定义多个名字和函数。这些名字对每一个 guard 都是可见的,这一来就避免了重复。若是咱们打算换种方式计算 bmi
,只需进行一次修改就好了。经过命名,咱们提高了代码的可读性,而且因为 bmi
只计算了一次,函数的执行效率也有所提高。咱们能够再作下修改:
bmiTell :: (RealFloat a) => a -> a -> 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 就搞不清楚它们在哪一个地方了。
where
绑定不会在多个模式中共享。若是你在一个函数的多个模式中重复用到同一名字,就应该把它置于全局定义之中。
where
绑定也可使用模式匹配!前面那段代码能够改为:
...
where bmi = weight / height ^ 2
(skinny, normal, fat) = (18.5, 25.0, 30.0)
咱们再搞个简单函数,让它告诉咱们姓名的首字母:
initials :: String -> String -> String
initials firstname lastname = [f] ++ ". " ++ [l] ++ "."
where (f:_) = firstname
(l:_) = lastname
咱们彻底按能够在函数的参数上直接使用模式匹配(这样更短更简洁),在这里只是为了演示在 where
语句中一样可使用模式匹配:
where
绑定能够定义名字,也能够定义函数。保持健康的编程语言风格,咱们搞个计算一组 bmi
的函数:
calcBmis :: (RealFloat a) => [(a, a)] -> [a]
calcBmis xs = [bmi w h | (w, h) <- xs]
where bmi weight height = weight / height ^ 2
这就全了!在这里将 bmi
搞成一个函数,是由于咱们不能依据参数直接进行计算,而必须先从传入函数的 List 中取出每一个序对并计算对应的值。
where
绑定还能够一层套一层地来使用。
有个常见的写法是,在定义一个函数的时候也写几个辅助函数摆在 where
绑定中。
而每一个辅助函数也能够透过 where
拥有各自的辅助函数。
let
绑定与 where
绑定很类似。where
绑定是在函数底部定义名字,对包括全部 guard 在内的整个函数可见。let
绑定则是个表达式,容许你在任何位置定义局部变量,而对不一样的 guard 不可见。正如 Haskell 中全部赋值结构同样,let
绑定也可使用模式匹配。看下它的实际应用!这是个依据半径和高度求圆柱体表面积的函数:
cylinder :: (RealFloat a) => a -> a -> a
cylinder r h =
let sideArea = 2 * pi * r * h
topArea = pi * r ^2
in sideArea + 2 * topArea
let
的格式为 let [bindings] in [expressions]
。在 let
中绑定的名字仅对 in
部分可见。let
里面定义的名字也得对齐到一列。不难看出,这用where
绑定也能够作到。那么它俩有什么区别呢?看起来无非就是,let
把绑定放在语句前面而 where
放在后面嘛。
不一样之处在于,let
绑定自己是个表达式,而 where
绑定则是个语法结构。还记得前面咱们讲if语句时提到它是个表达式,于是能够随处安放?
ghci> [if 5 > 3 then "Woo" else "Boo", if 'a' > 'b' then "Foo" else "Bar"]
["Woo", "Bar"]
ghci> 4 * (if 10 > 5 then 10 else 0) + 2
42
用 let
绑定也能够实现:
ghci> 4 * (let a = 9 in a + 1) + 2
42
let
也能够定义局部函数:
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!")
最后那个绑定后面的分号不是必须的,不过加上也不要紧。如咱们前面所说,你能够在 let
绑定中使用模式匹配。这在从 Tuple 取值之类的操做中很方便。
ghci> (let (a,b,c) = (1,2,3) in a+b+c) * 100
600
你也可以把 let
绑定放到 List Comprehension 中。咱们重写下那个计算 bmi
值的函数,用个 let
替换掉原先的where
。
calcBmis :: (RealFloat a) => [(a, a)] -> [a]
calcBmis xs = [bmi | (w, h) <- xs, let bmi = w / h ^ 2]
List Comprehension 中 let
绑定的样子和限制条件差很少,只不过它作的不是过滤,而是绑定名字。let
中绑定的名字在输出函数及限制条件中均可见。这一来咱们就可让咱们的函数只返回胖子的 bmi
值:
calcBmis :: (RealFloat a) => [(a, a)] -> [a]
calcBmis xs = [bmi | (w, h) <- xs, let bmi = w / h ^ 2, bmi >= 25.0]
在 (w, h) <- xs
这里没法使用 bmi
这名字,由于它在 let
绑定的前面。
在 List Comprehension 中咱们忽略了 let
绑定的 in
部分,由于名字的可见性已经预先定义好了。不过,把一个let...in
放到限制条件中也是能够的,这样名字只对这个限制条件可见。在 ghci 中 in
部分也能够省略,名字的定义就在整个交互中可见。
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
ghci> boot
< interactive>:1:0: Not in scope: `boot'
你说既然 let
已经这么好了,还要 where
干吗呢?嗯,let
是个表达式,定义域限制的至关小,所以不能在多个 guard 中使用。一些朋友更喜欢 where
,由于它是跟在函数体后面,把主函数体距离类型声明近一些会更易读。
有命令式编程语言 (C, C++, Java, etc.) 的经验的同窗必定会有所了解,不少命令式语言都提供了 case
语句。就是取一个变量,按照对变量的判断选择对应的代码块。其中可能会存在一个万能匹配以处理未预料的状况。
Haskell 取了这一律念融合其中。如其名,case
表达式就是,嗯,一种表达式。跟if..else
和 let
同样的表达式。用它能够对变量的不一样状况分别求值,还可使用模式匹配。Hmm,取一个变量,对它模式匹配,执行对应的代码块。好像在哪儿听过?啊,就是函数定义时参数的模式匹配!好吧,模式匹配本质上不过就是 case
语句的语法糖而已。这两段代码就是彻底等价的:
head' :: [a] -> a
head' [] = error "No head for empty lists!"
head' (x:_) = x
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
...
expression 匹配合适的模式。
一如预期地,第一个模式若匹配,就执行第一个区块的代码;不然就接下去比对下一个模式。若是到最后依然没有匹配的模式,就会产生运行时错误。
函数参数的模式匹配只能在定义函数时使用,而 case
表达式能够用在任何地方。例如:
describeList :: [a] -> String
describeList xs = "The list is " ++ case xs of [] -> "empty."
[x] -> "a singleton list."
xs -> "a longer list."
这在表达式中做模式匹配很方便,因为模式匹配本质上就是 case
表达式的语法糖,那么写成这样也是等价的:
describeList :: [a] -> String
describeList xs = "The list is " ++ what xs
where what [] = "empty."
what [x] = "a singleton list."
what xs = "a longer list." 转自:http://learnyouahaskell-zh-tw.csie.org/zh-cn/syntax-on-function.html