表达式

局部变量

迄今为止我们只掌握了如何定义孤立的全局函数或者变量(指常规意义下的变量,而非函数内的形参),但对于某些情况来说,定义全局变量或者函数并不合适,这就需要定义局部变量。

一方面定义局部变量可以限制作用域,使其在适当的区域发挥作用;另一方面,对于不同作用域的局部变量,便于重复使用变量名称。

let…in…

一个经典的情形是使用海伦公式计算三角形的面积,即给定三角形的三条边a,b,c,其面积如下:

\[S = \sqrt{p(p-a)(p-b)(p-c)} \ (p = \frac{a + b + c}{2})\]

这里大量重复地使用了p,当然我们可以直接将p全部带入计算,但这显然不是一个明智的决定。使用let…in…语法定义p则简洁很多。

-- 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语句。

-- 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函数举例。

-- 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..更像是一个运算符,因为这个表达式必然会返回一个值;相对地,顺序式编程语言中的条件表达式只是一种表述结构。

对于复杂冗长的条件表达式可以分多行书写:

-- 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为例,下面两种写法都是合法的。

-- 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对于条件表达式的缩进规则比较自由,你甚至可以写出一些比较怪异的风格:

bothzero'' :: Int -> Int -> Bool 
bothzero'' a b = 
  if a == 0 
then if b == 0
then True
else False
else False

级联条件表达式

Haskell也支持条件表达式的级联。

以比较两个整数大小为例,当第一个数大于第二个数,返回1;当两者相等返回0;否则返回-1。如下的写法均合法:

-- 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) 使用对其的|将函数的参数按照一定的条件表达式分类,这些条件将分别导致各自的结果。考虑布尔值的二元与运算:

-- 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

我们使用一种简单的调试方法,对上述事实进行验证。

--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''时,就可以看到实际的效果。

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示例,将其使用多分支条件表达式改写。

-- 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

-- 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函数中,我们并没有使用值构造器后所附带的形参,因此为了简便我们可以使用“万能牌”替代原有形参。

-- 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

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 .. 可以将分开匹配的模式的定义整合起来,如下:

-- 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]

例如当我们想要定义一个函数filterCircle,其从图形列表中筛选出所有的圆形形成新的列表。

-- 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才有意义,对于三角形还应当满足两边之和大于第三边。

-- 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]

补充: 实际上还有另外一种方案,可以将模式匹配和守卫结合到一起:

-- 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,用来判断两个图形的形状。如下:

-- 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"

这样通过模式守卫,我们可以避免嵌套的模式匹配以及复杂的条件分支。

补充: 实际上,不使用模式守卫也可以使用一定的技巧避免级联和复杂的条件分支:

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

模式同义

模式同义(Pattern Synonyms) 允许对模式匹配定义新名称,通过添加扩展{-# LANGUAGE PatternSynonyms #-}从而能够简化书写,提高可读性,减少写错的可能。详见参考链接Pattern Synonyms


[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.