25

这是我一直想知道的一个问题。if 语句是大多数编程语言中的主要内容(至少在我使用过的语言中),但在 Haskell 中,它似乎很不受欢迎。我知道对于复杂的情况,Haskell 的模式匹配比一堆 if 更干净,但是有什么真正的区别吗?

举个简单的例子,取一个自制版本的 sum (是的,我知道它可能只是foldr (+) 0):

sum :: [Int] -> Int
-- separate all the cases out
sum [] = 0
sum (x:xs) = x + sum xs

-- guards
sum xs
    | null xs = 0
    | otherwise = (head xs) + sum (tail xs)

-- case
sum xs = case xs of
    [] -> 0
    _ -> (head xs) + sum (tail xs)

-- if statement
sum xs = if null xs then 0 else (head xs) + sum (tail xs)

作为第二个问题,这些选项中的哪一个被认为是“最佳实践”,为什么?我的教授回到过去时总是尽可能使用第一种方法,我想知道这只是他的个人喜好还是背后有什么东西。

4

6 回答 6

47

您的示例的问题不在于if表达式,而在于使用部分函数,​​例如headand tail。如果您尝试使用空列表调用其中任何一个,则会引发异常。

> head []
*** Exception: Prelude.head: empty list
> tail []
*** Exception: Prelude.tail: empty list

如果在使用这些函数编写代码时出错,直到运行时才会检测到错误。如果您在模式匹配方面犯了错误,您的程序将无法编译。

例如,假设您不小心切换了函数的thenandelse部分。

-- Compiles, throws error at run time.
sum xs = if null xs then (head xs) + sum (tail xs) else 0

-- Doesn't compile. Also stands out more visually.
sum [] = x + sum xs
sum (x:xs) = 0

请注意,您的警卫示例也有同样的问题。

于 2013-05-15T18:36:06.187 回答
20

我认为Boolean Blindness文章很好地回答了这个问题。问题是布尔值在你构造它们时就失去了所有的语义意义。这使它们成为错误的重要来源,也使代码更难理解。

于 2013-05-15T19:16:01.203 回答
16

您的第一个版本是您的教授首选的版本,与其他版本相比具有以下优势:

  1. 没有提及null
  2. 列表组件在模式中被命名,所以没有提到headand tail

我确实认为这被认为是“最佳实践”。

有什么大不了的?为什么我们要特别避免headand tail?好吧,每个人都知道这些功能并不完整,因此人们会自动尝试确保涵盖所有情况。[] 上的模式匹配不仅比 更突出null xs,编译器还可以检查一系列模式匹配的完整性。因此,具有完整模式匹配的惯用版本更容易掌握(对于训练有素的 Haskell 阅读器)并且编译器可以证明详尽无遗。

第二个版本比上一个版本稍好,因为可以立即看到所有案件都已处理。尽管如此,在一般情况下,第二个等式的 RHS 可能更长,并且可能有一个带有几个定义的 where 子句,其中最后一个可能类似于:

where
   ... many definitions here ...
   head xs = ... alternative redefnition of head ...

要绝对确定了解 RHS 的作用,必须确保未重新定义常用名称。

第三个版本是最糟糕的一个恕我直言:a)第二场比赛未能解构列表,仍然使用头和尾。b) 该案例比具有 2 个等式的等效表示法略为冗长。

于 2013-05-15T18:48:56.300 回答
8

在许多编程语言中,if 语句是基本的原语,而 switch 块之类的东西只是语法糖,可以使深度嵌套的 if 语句更容易编写。

Haskell 则相反。模式匹配是基本的原语,if 表达式实际上只是模式匹配的语法糖。类似地,类似nullhead的结构只是用户定义的函数,它们最终都是使用模式匹配来实现的。所以模式匹配是最重要的。(因此可能比调用用户定义的函数更有效。)

在许多情况下 - 例如您上面列出的那些 - 这只是风格问题。编译器几乎可以肯定地将事物优化到所有版本的性能大致相同的程度。但通常[并非总是如此!] 模式匹配可以让您更清楚地了解您想要实现的目标。

(编写一个 if 表达式非常容易,如果你以错误的方式得到两个替代方案。你会认为这将是一个罕见的错误,但它却出奇地普遍。使用模式匹配,犯那个特定错误的机会很小,尽管还有很多其他事情要搞砸。)

于 2013-05-15T19:54:07.983 回答
6

每次调用nullheadtail需要一个模式匹配。但是您答案中的第一个版本只匹配一个模式,并通过模式的命名组件重用其结果。

仅此,它就更好了。但它也更直观,更易读。

于 2013-05-15T18:58:53.790 回答
5

模式匹配优于 if-then-else 语句字符串,原因如下:

  1. 它更具声明性
  2. 它与 sum-types 很好地交互

模式匹配有助于减少代码中的“意外复杂性”——也就是说,代码实际上更多的是关于实现细节而不是程序的基本逻辑。

在大多数其他语言中,当编译器/运行时看到一串 if-then-else 语句时,它别无选择,只能按照程序员指定的顺序测试条件。但是模式匹配鼓励程序员更多地关注描述应该发生什么而不是应该如何执行。由于 Haskell 中值的纯度和不变性,编译器可以将模式集合视为一个整体,并决定如何最好地实现它们。

类比是 C 的switch陈述。如果您转储各种 switch 语句的汇编代码,您会看到有时编译器会生成比较链/树,而在其他情况下,它会生成跳转表。程序员在这两种情况下都使用相同的语法——编译器根据比较值选择实现。如果它们形成一个连续的值块,则使用跳转表方法,否则使用比较树。如果检测到比较值中的其他模式,这种关注点分离允许编译器在未来实现更多策略。

于 2013-05-15T19:38:47.553 回答