模式匹配和守卫之间有什么区别?

68
我很新手,对Haskell和函数式编程都不熟悉。我的问题非常基础。模式匹配和Guard之间有什么区别? 使用模式匹配的函数
check :: [a] -> String
check [] = "Empty"
check (x:xs) = "Contains Elements"

使用 guards 的函数

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

在我看来,模式匹配和Guard实际上是相同的。两者都会评估一个条件,如果为真,则执行与之相关联的表达式。我的理解正确吗?

在这个例子中,我可以使用模式匹配或guard来得到相同的结果。但是我感觉我错过了一些重要的东西。我们总是可以用一个替换另一个吗?

能否给出一些示例,说明何时更喜欢使用模式匹配而不是guard,反之亦然?


3
这句话的意思是:这与你的问题没有直接关系,但函数的第一个版本可能更好,因为它更有效率并且可以适用于无限列表。 - Daniel Pratt
15
模式匹配关注数据的形式,守卫关注数据的内容 - Dario
请查看Haskell语言报告中模式匹配的定义 - Don Stewart
4个回答

68
实际上,它们在本质上是非常不同的!至少在Haskell中是这样的。
卫语句更简单、更灵活:它们本质上只是一种特殊的语法,可以转换为一系列的if/then表达式。你可以在卫语句中放置任意布尔表达式,但它们并没有做任何你不能用常规if做到的事情。
模式匹配还有几个额外的功能:它们是解构数据的唯一方法,并在其作用域内绑定标识符。就像卫语句等价于if表达式一样,模式匹配等价于case表达式。声明(无论是在顶层还是在类似let表达式之类的东西中)也是模式匹配的一种形式,其中“普通”定义是与平凡模式(一个标识符)匹配的。
模式匹配也往往是Haskell中实际发生的事情的主要方式——尝试在模式中解构数据是强制求值的少数几件事之一。
顺便说一下,你实际上可以在顶层声明中进行模式匹配:
square = (^2)

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

这对于一组相关定义偶尔是有用的。

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


关于日常使用哪个,以下是一些粗略的指南:

  • 对于可以直接匹配一两个构造函数深度的任何内容,您肯定要使用模式匹配,其中您不真正关心复合数据作为整体,但确实关心大部分结构。 @ 语法允许您将整体结构绑定到变量,同时还可以在其上进行模式匹配,但是在一个模式中做太多这样的操作可能会很丑陋和难以阅读。

  • 当您需要基于某些属性进行选择时,而这些属性与模式没有明显的对应关系时,例如比较两个Int值以查看哪个更大,肯定要使用 guards。

  • 如果您只需要从大型结构内部获取几个数据,并且特别是如果您还需要将结构作为整体使用,则 guards 和访问器函数通常比充满@_的庞大模式更易读。

  • 如果您需要针对由不同模式表示的值执行相同的操作,但具有方便的谓词来对其进行分类,则使用带有 guard 的单个通用模式通常更易读。请注意,如果一组 guards 不完整,则未通过所有 guards 的任何内容都将下降到下一个模式(如果有)。因此,您可以将一般模式与某些过滤器组合起来以捕获异常情况,然后对其他所有内容进行模式匹配以获取您关心的详细信息。

  • 绝对不要使用 guards 来检查可以使用模式轻松检查的内容。检查空列表是经典示例,请使用模式匹配。

  • 总的来说,当您不确定时,只需默认使用模式匹配,通常更好。如果模式开始变得非常丑陋或复杂,则停下来考虑如何以其他方式编写它。除了使用 guards 之外,其他选项包括提取子表达式作为单独的函数或在函数体内放置case表达式,以便将一些模式匹配推入其中并从主定义中排除。


5
不要忘记他们的宝宝,模式守卫! - luqui
1
@luqui:啊,是的!这是我疏忽了,谢谢你提醒我。 - C. A. McCann

11

首先,你可以在守卫中放置布尔表达式。

例如

像列表推导一样,布尔表达式可以自由地与模式守卫混合使用。例如:

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

这里有一段来自Haskell入门教程的优秀引用,用于解释二者之间的区别:

模式匹配是一种确保值符合某种形式并对其进行分析的方式,而卫语句则是测试一个或多个值是否满足某个属性的方法。听起来很像if语句,而且非常相似。不同的是,当你有几个条件时,卫语句更容易读,而且它们与模式匹配非常搭配。


1
值得注意的是,这不是标准的 Haskell,而是 GHC 的扩展。 - Antal Spector-Zabusky
2
另一方面,在最流行的编译器中,纯语法扩展是非常安全的使用方式。最坏的情况是,为了移植到其他编译器,您必须稍后手动对其进行解糖处理。 - C. A. McCann
3
@Antal,实际上这是标准的Haskell 2010版本。http://www.haskell.org/onlinereport/haskell2010/haskellch3.html#x8-460003.13 - luqui

10
对我来说,模式匹配和守卫在根本上是相同的。两者都评估一个条件,如果为真,就会执行与其关联的表达式。我的理解正确吗?
不完全正确。首先,模式匹配无法评估任意条件。它只能检查一个值是否使用给定的构造函数创建。
第二,模式匹配可以绑定变量。因此,虽然模式[]可能等价于守卫null lst(不使用length因为那不等价-稍后会讲到),但模式x:xs肯定不等价于守卫not(null lst),因为模式绑定了变量x和xs,而守卫没有。
关于使用length的说明:使用length来检查列表是否为空是非常糟糕的实践,因为为了计算长度,它需要遍历整个列表,这将需要O(n)时间,而仅检查列表是否为空则使用null或模式匹配需要O(1)时间。此外,使用`length´在无限列表上根本不起作用。

6

除了其他好的答案,我会尝试具体说明守卫:守卫只是语法糖。如果你仔细思考,你的程序中经常有以下结构:

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

也就是说,如果一个模式匹配成功,紧接着会执行if-then-else判断。使用guard可以将这个判断直接合并到模式匹配中:
f y = ...
f x | p(x) = A
    | otherwise = B

在标准库中,otherwise被定义为True。它比if-then-else语句更方便,有时也可以使代码更简单,易于编写。

换句话说,在很多情况下它是另一种构造的糖衣,可以极大地简化您的代码。您会发现它可以消除许多if-then-else语句,使您的代码更易读。


1
我会写 p x 而不是 p(x) - mk12

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接