# 表达式 ## 局部变量 迄今为止我们只掌握了如何定义孤立的全局函数或者变量(指常规意义下的变量,而非函数内的形参),但对于某些情况来说,定义全局变量或者函数并不合适,这就需要定义局部变量。 一方面定义局部变量可以限制作用域,使其在适当的区域发挥作用;另一方面,对于不同作用域的局部变量,便于重复使用变量名称。 ### let...in... 一个经典的情形是使用海伦公式计算三角形的面积,即给定三角形的三条边`a`,`b`,`c`,其面积如下: ```math S = \sqrt{p(p-a)(p-b)(p-c)} \ (p = \frac{a + b + c}{2}) ``` 这里大量重复地使用了`p`,当然我们可以直接将`p`全部带入计算,但这显然不是一个明智的决定。使用let...in...语法定义`p`则简洁很多。 ```haskell -- code2.hs heron's_formula :: Double -> Double -> Double -> Double heron's_formula a b c = let p = (a + b + c)/2 in sqrt (p * (p - a) * (p - b) * (p - c)) ``` 当实际使用到`heron's_formula`函数时,`p`会自动被替换为`(a+b+c)/2`,在计算时只计算一次`p`即可,因此还节约了代码运行时间。 ### where 除了使用let...in...还可以使用where语句。 ```haskell -- code2.hs heron's_formula' :: Double -> Double -> Double -> Double heron's_formula' a b c = sqrt (p * (p - a) * (p - b) * (p - c)) where p = (a + b + c)/2 ``` 使用where语句与let...in...语句类似,在做计算时会先计算`p`再带入到定义中。 > 提示:应当注意对于内层作用域中出现重复的变量名时,外部作用域相应的变量名会被覆盖掉,例如`let x = 1 in x + let x = 2 in x + let x = 3 in x`的结果为6 ## 条件表达式 Haskell的条件表达式格式为if..then..else..,我们继续用上一章讲解类型多态中提到的`divide`函数举例。 ```haskell -- code2.hs divide :: Int -> Int -> Maybe Int divide a b = if b == 0 then Nothing else Just (a `div` b) ``` `if`后跟随条件表达式(即类型为`Bool`),当条件表达式的值为`True`则返回`then`后面表达式的值,否则返回`else`后面表达式的值。这里当除数`b`为0时,整除无意义,返回`Nothing`;当`b`不为0时,则计算并返回用`Just`包裹的结果。 > 提示:如果读者了解一些顺序式编程语言的话,那么就会发现Haskell中条件表达式略有不同。在Haskell中if..then..else..更像是一个运算符,因为这个表达式必然会返回一个值;相对地,顺序式编程语言中的条件表达式只是一种表述结构。 对于复杂冗长的条件表达式可以分多行书写: ```haskell -- code2.hs divide' :: Int -> Int -> Maybe Int divide' a b = if b == 0 then Nothing else Just (a `div` b) divide'' :: Int -> Int -> Maybe Int divide'' a b = if b == 0 then Nothing else Just (a `div` b) ``` 上述两种都是合法的表达式。 ### 嵌套条件表达式 Haskell支持条件表达式的嵌套。 以判断两个整数是否都是0为例,下面两种写法都是合法的。 ```haskell -- code2.hs bothzero :: Int -> Int -> Bool bothzero a b = if a == 0 then if b == 0 then True else False else False bothzero' :: Int -> Int -> Bool bothzero' a b = if a == 0 then if b == 0 then True else False else False ``` > 提示:读者可以自行尝试其他可能的写法,Haskell对于条件表达式的缩进规则比较自由,你甚至可以写出一些比较怪异的风格: > ```haskell > bothzero'' :: Int -> Int -> Bool > bothzero'' a b = > if a == 0 > then if b == 0 > then True > else False > else False > ``` ### 级联条件表达式 Haskell也支持条件表达式的级联。 以比较两个整数大小为例,当第一个数大于第二个数,返回1;当两者相等返回0;否则返回-1。如下的写法均合法: ```haskell -- code2.hs compareToInt :: Int -> Int -> Int compareToInt a b = if a > b then 1 else if a == b then 0 else -1 compareToInt' :: Int -> Int -> Int compareToInt' a b = if a > b then 1 else if a == b then 0 else -1 ``` ## guard 守卫 ***守卫(Guard)*** 使用对其的`|`将函数的参数按照一定的条件表达式分类,这些条件将分别导致各自的结果。考虑布尔值的二元与运算: ```haskell -- code2.hs and' :: Bool -> Bool -> Bool and' a b | a == True = b | otherwise = False ``` > 提示: haskell内置了`and`函数用于将装有容器的布尔值做与运算,这里为了避开其名称,改为了`and'` 当`a`的值为`True`时,函数返回`b`;当`a`的值不为`True`时,即其他情况`otherwise`,函数返回`False`。 可以看到,守卫的效果与(级联)条件表达式类似,自上而下执行条件表达式。虽然我们期望守卫中每个条件表达式包含的情况没有交集,但实际上,Haskell允许条件表达式之间产生交集,且表达式实际守卫的条件是其所包含的条件与前面所有守卫的条件表达式包含条件的差集,或者通俗来讲——先入为主。 例如我们有若干条件表达式,他们分别产生了函数的定义域集合`A`,`B`,`C`,如果使用守卫对其进行排序,不妨`A B C`,则Haskell返回`B`对应结果的条件是`B - A`;返回`C`对应结果的条件是`C - A - B` 我们使用一种简单的调试方法,对上述事实进行验证。 ```haskell --code2.hs import Debug.Trace( trace ) and'' :: Bool -> Bool -> Bool and'' a b | a = trace "first condition" b | b = trace "second condition" a | otherwise = trace "otherwise condition" False ``` `and''`使用`Debug.Trace`模块中的`trace`函数来“追踪”守卫到底运行了哪条条件表达式,当我们通过GHCi使用`and''`时,就可以看到实际的效果。 ```bash Prelude> :load code2.hs [1 of 2] Compiling Main ( code2.hs, interpreted ) Ok, one module loaded. Prelude> and'' True False first condition False Prelude> and'' True True first condition True Prelude> and'' False True second condition False Prelude> and'' False False otherwise condition False ``` > 补充:细心的读者会发现如果将`otherwise`放到守卫表达式中非末尾的位置,那么程序不仅没有报错,其后面的条件表达式将永远无法执行;确实,如果使用GHCi查看`otherwise`的类型,可以发现其是一个布尔值,更进一步地,`otherwise`的值是`True`。 ## 多分支条件表达式 虽然守卫对于多分支条件表达相比if..then..else简洁很多,但守卫的使用相对局限,一个有用的扩展`{-# LANGUAGE MultiWayIf #-}`允许使用守卫将if..then..else条件表达式简化。 沿用条件表达式中的`bothzero`示例,将其使用多分支条件表达式改写。 ```haskell -- code2.hs {-# LANGUAGE MultiWayIf #-} bothzero'' :: Int -> Int -> Bool bothzero'' a b = if | a == 0 -> if | b == 0 -> True | otherwise -> False | otherwise -> False ``` ## 模式匹配 ***模式匹配(Pattern Match)*** 是另一个基础而重要的用法,其主要用来匹配特定类型中数据的形式。 我们沿用上一章的`Figure''`定义,并为其添加一个判断形状的函数`judgeShape`。 ```haskell -- code2.hs -- 圆形直径 type Diameter = Double -- 长方形长宽 type Length = Double type Width = Double -- 三角形三条边长 type Side1 = Double type Side2 = Double type Side3 = Double data Figure'' = Circle'' { getDiameter :: Diameter} | Rectangle'' { getLength :: Length, getWidth :: Width} | Triangle'' { getSide1 :: Side1, getSide2 :: Side2, getSide3 :: Side3 } deriving (Show) judgeShape :: Figure'' -> String judgeShape (Circle'' diameter) = "This is a Circle" judgeShape (Rectangle'' length width) = "This is a Rectangle" judgeShape (Triangle'' side1 side2 side3) = "This is a Triangle" ``` 可以看到`judgeShape`函数的每一行对应了一种形状,其接受的参数不再是一个抽象的符号(如`a`,`b`之类),而是一个由值构造器构造出来的值。 ### Wide Card `judgeShape`函数中,我们并没有使用值构造器后所附带的形参,因此为了简便我们可以使用“万能牌”替代原有形参。 ```haskell -- code2.hs judgeShape' :: Figure'' -> String judgeShape' (Circle'' _) = "This is a Circle" judgeShape' (Rectangle'' _ _) = "This is a Rectangle" judgeShape' (Triangle'' _ _ _) = "This is a Triangle" ``` > 补充: 对于记录语法定义的数据类型,我们可以使用`{}`作为record版本的Wild Card > ```haskell > judgeShape'' :: Figure'' -> String > judgeShape'' (Circle'' _) = "This is a Circle" > judgeShape'' (Rectangle'' {}) = "This is a Rectangle" > judgeShape'' (Triangle'' {}) = "This is a Triangle" > ``` ### case .. of .. 使用case .. of .. 可以将分开匹配的模式的定义整合起来,如下: ```haskell -- code2.hs judgeShape''' :: Figure'' -> String judgeShape''' x = case x of Circle'' _ -> "This is a Circle" Rectangle'' {} -> "This is a Rectangle" Triangle'' {} -> "This is a Triangle" ``` ### as 模式 ***as模式(As-patterns)*** 提供将固定模式绑定到形参的功能[[1]](#ref1)。 例如当我们想要定义一个函数`filterCircle`,其从图形列表中筛选出所有的圆形形成新的列表。 ```haskell -- code2.hs filterCircle :: [Figure''] -> [Figure''] filterCircle ls = case ls of [] -> [] (x@(Circle'' _):xs) -> x : filterCircle xs _:xs -> filterCircle xs ``` `filterCirlce`首先匹配空列表,返回为空;当列表非空且头部为`Circle'' _`时,使用`@`将其绑定到`x`上,并将其作为返回列表的头部,然后递归地处理尾部;最后对于非空列表但头部为除圆外的模式时,抛弃头部,递归地处理尾部。 ## 模式守卫 有时我们不仅需要匹配模式,还需要在特定模式下满足一定条件,如果使用模式匹配再使用if...then...else语句会显得很麻烦。使用 ***模式守卫(Pattern Guards)*** 可以很好地将模式匹配和守卫结合起来,以满足我们的需要。仍然以`judgeShape`函数为例,我们至少应当保证每种图形中的量大于0才有意义,对于三角形还应当满足两边之和大于第三边。 ```haskell -- code2.hs -- {-# LANGUAGE PatternGuards #-} judgeShape_4 :: Figure'' -> String judgeShape_4 x | Circle'' r <- x, r > 0 = "This is a Circle" | Rectangle'' l w <- x, l > 0, w > 0 = "This is a Rectangle" | Triangle'' s1 s2 s3 <- x, s1 > 0, s2 > 0, s3 > 0, s1 + s2 > s3, s1 + s3 > s2, s2 + s3 > s3 = "This is A Triangle" | otherwise = "Invalid Figure" ``` > 提示:Haskell 2010标准下,PatternGuards 扩展已经被作为标准语法,因此无需使用扩展声明语句[[2]](#ref2) > 补充: 实际上还有另外一种方案,可以将模式匹配和守卫结合到一起: > ```haskell > -- code2.hs > judgeShape_5 :: Figure'' -> String > judgeShape_5 x = > case x of > Circle'' r | r > 0 -> "This is a Circle" > Rectangle'' l w | l > 0, w > 0 -> "This is a Rectangle" > Triangle'' s1 s2 s3 | s1 > 0 , s2 > 0 , s3 > 0 , > s1 + s2 > s3, s1 + s3 > s2, s2 + s3 > s3 -> "This is A Triangle" > _ -> "Invalid Figure" > ``` > 这种方案更适合对于一个模式下有多条件分支的情况 另外,模式守卫还允许一次性匹配多个模式,我们在`judgeShape_5`的基础上改变一下得到`judge2Shapes`,用来判断两个图形的形状。如下: ```haskell -- code2.hs judge2Shapes :: Figure'' -> Figure'' -> String judge2Shapes x y | Circle'' rx <- x, Circle'' ry <- y, rx > 0, ry > 0 = "Two Circles" | Circle'' rx <- x, Rectangle'' ly wy <- y, rx > 0, ly > 0 && wy > 0 ="A Circle and a Rectangle" | Rectangle'' lx wx <- x, Circle'' ry <- y, lx > 0 && wx > 0, ry > 0 = "A Circle and a Rectangle" | Circle'' rx <- x, Triangle'' s1y s2y s3y <- y, rx > 0, s1y > 0 && s2y > 0 && s3y > 0, s1y + s2y > s3y && s2y + s3y > s1y && s1y + s3y > s2y = "A Circle and a Triangle" | Triangle'' s1x s2x s3x <- x, Circle'' ry <- y, s1x > 0 && s2x > 0 && s3x > 0, s1x + s2x > s3x && s2x + s3x > s1x && s1x + s3x > s2x, ry > 0 = "A Circle and a Triangle" | Rectangle'' lx wx <- x, Rectangle'' ly wy <- y, lx > 0 && wx > 0, ly > 0 && wy > 0 = "Two Rectangles" | Rectangle'' lx wx <- x, Triangle'' s1y s2y s3y <- y, lx > 0 && wx > 0, s1y > 0 && s2y > 0 && s3y > 0, s1y + s2y > s3y && s2y + s3y > s1y && s1y + s3y > s2y = "A Rectangle and a Triangle" | Triangle'' s1x s2x s3x <- x, Rectangle'' ly wy <- y, s1x > 0 && s2x > 0 && s3x > 0, s1x + s2x > s3x && s2x + s3x > s1x && s1x + s3x > s2x, ly > 0 && wy > 0 = "A Rectangle and A Triangle" | Triangle'' s1x s2x s3x <- x, Triangle'' s1y s2y s3y <- y, s1x > 0 && s2x > 0 && s3x > 0, s1x + s2x > s3x && s2x + s3x > s1x && s1x + s3x > s2x, s1y > 0 && s2y > 0 && s3y > 0, s1y + s2y > s3y && s2y + s3y > s1y && s1y + s3y > s2y = "Two Triangles" | otherwise = "Invalid Figures" ``` 这样通过模式守卫,我们可以避免嵌套的模式匹配以及复杂的条件分支。 > 补充: 实际上,不使用模式守卫也可以使用一定的技巧避免级联和复杂的条件分支: > ```haskell > judge2Shapes' :: Figure'' -> Figure'' -> String > judge2Shapes' x y = > case (x,y) of > (Circle'' rx, Circle'' ry) | rx > 0 , ry > 0 -> "Two Circles" > ... > ``` > 同样这种方式也比较适合一个模式下有多条件分支的情况 ## 观察模式 ***观察模式(View Patterns)*** 与模式守卫类似,通过添加扩展`{-# LANGUAGE ViewPatterns #-}`可以避免嵌套模式匹配的情况。这里给出链接以供读者参考[View Patterns](https://downloads.haskell.org/ghc/latest/docs/users_guide/exts/view_patterns.html)。 ## 模式同义 ***模式同义(Pattern Synonyms)*** 允许对模式匹配定义新名称,通过添加扩展`{-# LANGUAGE PatternSynonyms #-}`从而能够简化书写,提高可读性,减少写错的可能。详见参考链接[Pattern Synonyms](https://downloads.haskell.org/ghc/latest/docs/users_guide/exts/pattern_synonyms.html)。 -------------------------------
[1] Haskell/Pattern matching. (2023, April 6). Wikibooks. Retrieved 05:03, March 17, 2024 from https://en.wikibooks.org/w/index.php?title=Haskell/Pattern_matching&oldid=4276286.
[2] Future of Haskell. (2023, January 28). HaskellWiki, . Retrieved 07:10, March 17, 2024 from https://wiki.haskell.org/index.php?title=Future_of_Haskell&oldid=65510.