为什么在函数定义中偏爱模式匹配?

6

我正在阅读来自learnyouahaskell的"learnyouahaskell"教程。其中有这样一段内容:

Pattern matching can also be used on tuples. What if we wanted to make a function that takes two vectors in a 2D space (that are in the form of pairs) and adds them together? To add together two vectors, we add their x components separately and then their y components separately. Here's how we would have done it if we didn't know about pattern matching:

addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a)  
addVectors a b = (fst a + fst b, snd a + snd b)  

Well, that works, but there's a better way to do it. Let's modify the function so that it uses pattern matching.

addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a)  
addVectors (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)  

There we go! Much better. Note that this is already a catch-all pattern. The type of addVectors (in both cases) is addVectors :: (Num a) => (a, a) -> (a, a) - > (a, a), so we are guaranteed to get two pairs as parameters.

我的问题是:如果两种定义的结果相同,为什么模式匹配更受青睐?

我们来比较一下 - 哪个更易读呢?这样做是不是更加美观,更接近实际定义呢?你甚至不需要解析/理解 fstsnd 就能理解函数... - Random Dev
请不要误解,这只是一个询问意见的问题,没有“正确”的答案(或任何正确的答案)- 这就是为什么我投票关闭它。 - Random Dev
@Carsten Hm,你可能对可读性有一点看法。我读了它,想着:“但是在更多的代码中也是这样的。” 现在你提到一个人不必知道 fstsnd,我可以看出那可能会有可读性的优势。 - Zelphir Kaltstahl
如果您进行一些微妙的布局修改,例如 addVectors (x₁,y₁) (x₂,y₂) = (x₁+x₂, y₁+y₂),那么模式匹配版本看起来会更好。 - leftaroundabout
3个回答

5

我认为在这种情况下,模式匹配更直接地表达了你的意思。

在函数应用的情况下,需要知道 fstsnd 的作用,并从中推断出 ab 是元组,其元素会被相加。

addVectors a b = (fst a + fst b, snd a + snd b)

在这里,我们有sndfst函数来分解元组,这事实上是个干扰。

在模式匹配的情况下,我们可以立即看出输入是什么(一个元组,其元素称为x1y1等),以及如何将其拆解。同时,我们也能清楚地看到正在发生什么,它们的元素是如何相加的。

addVectors (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)

这几乎就像是数学定义:

(x1, y1) + (x2, y2) := (x1 + x2, y1 + y2)

直奔主题,没有分散注意力的内容:-)

你可以在Haskell中直接编写此代码:

(x₁, y₁) `addVector` (x₂, y₂) = (x₁ + x₂, y₁ + y₂)

3
简而言之,需要构建和销毁值。
通过使用数据构造器(可能是空的函数)并应用所需参数来构建值。到目前为止,一切都很好。
随机示例(滥用 GADTSyntax):
data T where
  A :: Int -> T
  B :: T
  C :: String -> Bool -> T

破坏(Destruction)更为复杂,因为需要获取类型 T 的值,并获得以下信息:1)使用哪个构造函数来创建该值,以及2)该构造函数的参数是什么。

第一部分可以通过一个函数完成:

whichConsT :: T -> Int -- returns 0,1,2 for A,B,C

第二部分比较棘手。一个可能的选择是使用投影。

projA :: T -> Int
-- projB not needed
projC1 :: T -> String
projC2 :: T -> Bool

以便例如它们满足

projA (A n) = n
projC1 (C x y) = x
projC2 (C x y) = y

但是等等!这些投影的类型形式为T -> ...,这意味着这样的函数适用于所有T类型的值。因此我们可以有:

projA B = ??
projA (C x y) = ??
projC1 (A n) = ??

如何实施上述内容?由于无法产生合理的结果,所以最好的选择是触发运行时错误。
projA B = error "not an A!"
projA (C x y) = error "not an A!"
projC1 (A n) = error "not a C!"

不过,这会给程序员带来一定的负担!现在,程序员需要检查传递给投影的值是否具有正确的构造函数。可以使用whichConsT进行检查。许多命令式编程人员习惯于这种接口(测试和访问,例如Java中迭代器的hasNext(),next()),但这是因为大多数命令式语言没有更好的选择。

函数式语言(以及现在一些命令式语言)也允许模式匹配。与投影相比,使用模式匹配具有以下优点:

  • 无需拆分信息:我们同时获取1)和2)
  • 无法使程序崩溃:我们从不使用可能会崩溃的部分投影函数
  • 不会给程序员增加负担:以上述为推论
  • 如果启用了穷举性检查,我们可以确保处理所有可能的情况

现在,在仅有一个构造函数的类型(元组,()newtype)上,可以定义完全的投影,并且这很好(例如fst,snd)。尽管如此,许多人更喜欢坚持使用模式匹配,因为它也可以处理一般情况。


2
正如评论中的Carsten所提到的,这是一个基于观点的问题,但是让我详细说明一下。对2元组进行模式匹配并没有太大的优势,但是让我们考虑一些更大的数据结构,例如4元组。
addVectors :: (Num a) => (a, a, a, a) -> (a, a, a, a) -> (a, a, a, a)  
addVectors a b = -- some code that adds vectors

addVectors :: (Num a) => (a, a, a, a) -> (a, a, a, a) -> (a, a, a, a)  
addVectors (w1, x1, y1, z1) (w2, x2, y2, z2) = (w1 + w2, x1 + x2, y1 + y2, z1 + z2)

如果没有模式匹配,你需要编写函数来从4元组中提取第一个、第二个、第三个和第四个元素,并在addVectors内使用它。有了模式匹配,编写addVectors的实现非常容易。

我认为在书中使用这样的例子可以更有效地传达信息。


addVectors a b = (a^._1 + b^._1, a^._2 + b^._2, a^._3 + b^._3, a^._4 + b^._4)。此外,我认为使用四元组作为例子并不太明智,因为基本上没有人使用大于三的元组。可以使用类似列表cons单元格的模式来正确传达消息。 - leftaroundabout

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