64

我对 Haskell 和一般的函数式编程非常陌生。我的问题很基本。模式匹配和守卫有什么区别?

使用模式匹配的函数

check :: [a] -> String
check [] = "Empty"
check (x:xs) = "Contains Elements"

使用守卫的功能

check_ :: [a] -> String
check_ lst
    | length lst < 1 = "Empty"
    | otherwise = "Contains elements"

在我看来,模式匹配和警卫基本上是相同的。两者都评估一个条件,如果为真,将执行与它挂钩的表达式。我的理解正确吗?

在这个例子中,我可以使用模式匹配或守卫来获得相同的结果。但是有些事情告诉我,我在这里错过了一些重要的事情。我们可以总是用另一种代替吗?

有人可以举例说明模式匹配优于守卫,反之亦然?

4

4 回答 4

65

实际上,它们本质上是完全不同的!至少在 Haskell 中,无论如何。

守卫既简单又灵活:它们本质上只是特殊语法,可以转换为一系列 if/then 表达式。你可以在守卫中放置任意的布尔表达式,但它们不会做任何你不能用常规做的事情if

模式匹配做了几件额外的事情:它们是解构数据的唯一方法,并且它们在其范围内绑定标识符。就像守卫等同于if表达式一样,模式匹配等同于case表达式。声明(无论是在顶层,还是在类似let表达式的地方)也是一种模式匹配形式,“正常”定义与普通模式匹配,即单个标识符。

模式匹配也往往是 Haskell 中实际发生的主要方式——尝试解构模式中的数据是强制评估的少数事情之一。

顺便说一句,您实际上可以在顶级声明中进行模式匹配:

square = (^2)

(one:four:nine:_) = map square [1..]

这有时对一组相关定义很有用。

GHC 还提供了 ViewPatterns 扩展,它结合了两者;您可以在绑定上下文中使用任意函数,然后对结果进行模式匹配。当然,这仍然只是普通东西的语法糖。


至于日常问题用在哪里,这里有一些粗略的指南:

  • 对于可以直接匹配一两个构造函数的任何内容,绝对使用模式匹配,在这种情况下,您并不真正关心整个复合数据,而是关心大部分结构。该@语法允许您将整个结构绑定到一个变量,同时还对其进行模式匹配,但是在一个模式中做太多的事情会很快变得丑陋和不可读。

  • 当您需要根据一些与模式不完全对应的属性做出选择时,一定要使用守卫,例如比较两个Int值以查看哪个更大。

  • 如果您只需要来自大型结构内部的几条数据,特别是如果您还需要将结构作为一个整体使用,那么守卫和访问器函数通常比一些充满@and的怪异模式更具可读性_

  • 如果您需要对由不同模式表示的值执行相同的操作,但使用方便的谓词对它们进行分类,则使用带有保护的单个通用模式通常更具可读性。请注意,如果一组守卫不是详尽无遗的,那么所有守卫失败的任何事情都将下降到下一个模式(如果有的话)。因此,您可以将通用模式与一些过滤器结合起来以捕获异常情况,然后对其他所有内容进行模式匹配以获得您关心的细节。

  • 绝对不要将守卫用于可以用模式进行简单检查的事情。检查空列表是典型的例子,为此使用模式匹配。

  • 一般来说,当有疑问时,只要坚持默认模式匹配,它通常会更好。如果一个模式开始变得非常丑陋或令人费解,那么请停下来考虑你还能如何编写它。除了使用守卫之外,其他选项包括将子表达式提取为单独case的函数或将表达式放入函数体中,以便将一些模式匹配向下推送到它们上并从主定义中移出。

于 2010-11-11T16:44:22.003 回答
10

一方面,您可以将布尔表达式放在警卫中。

例如

就像列表推导一样,布尔表达式可以在模式保护中自由混合。例如:

f x | [y] <- x
    , y > 3
    , Just z <- h y
    = ...


更新

Learn You a Haskell中有一段很好的引述,关于差异:

模式是一种确保值符合某种形式并对其进行解构的方法,而守卫是一种测试值的某些属性(或其中几个属性)是真还是假的方法。这听起来很像 if 语句,而且非常相似。问题是当你有几个条件时,守卫更具可读性,并且它们与模式的配合非常好。

于 2010-11-11T16:37:09.183 回答
10

在我看来,模式匹配和警卫基本上是相同的。两者都评估一个条件,如果为真,将执行与它挂钩的表达式。我的理解正确吗?

不完全的。第一个模式匹配不能评估任意条件。它只能检查一个值是否是使用给定的构造函数创建的。

第二个模式匹配可以绑定变量。因此,虽然模式[]可能等同于守卫null lst(不使用长度,因为那不等同 - 稍后会详细介绍),但模式x:xs肯定不等同于守卫not (null lst),因为模式绑定了变量xxs,守卫所做的不是。

关于使用的注意事项length:使用length来检查列表是否为空是非常糟糕的做法,因为要计算需要遍历整个列表的长度,这需要O(n)时间,而仅检查列表是否为空需要O(1)时间null模式匹配。进一步使用'length' just plain 不适用于无限列表。

于 2010-11-11T16:43:18.093 回答
5

除了其他好的答案之外,我将尝试具体说明守卫:守卫只是语法糖。如果您考虑一下,您的程序中通常会具有以下结构:

f y = ...
f x =
  if p(x) then A else B

也就是说,如果一个模式匹配,则紧随其后的是 if-then-else 判别。守卫将这种区分直接折叠到模式匹配中:

f y = ...
f x | p(x) = A
    | otherwise = B

otherwise被定义True在标准库中)。它比 if-then-else 链更方便,有时它还使代码变体更简单,因此比 if-then-else 结构更容易编写。

换句话说,它是另一种结构之上的糖,在许多情况下大大简化了您的代码。您会发现它消除了许多 if-then-else 链,并使您的代码更具可读性。

于 2010-11-11T16:44:28.437 回答