这是一个我一直想知道的问题。如果语句是大多数编程语言中的主要语言(至少是我曾经使用过的语句),但在Haskell中,它似乎很不受欢迎。我理解,对于复杂的情况,Haskell的模式匹配比一堆ifs更清晰,但是有什么真正的区别吗?
举一个简单的例子,拿一个自制版本的总和(是的,我知道它可能只是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)
作为第二个问题,哪一个选项被认为是“最佳实践”,为什么?我的教授回来的时候总是尽可能地使用第一种方法,我想知道这是否仅仅是他个人的偏好,或者是否有背后的东西。
答案 0 :(得分:46)
您的示例的问题不是if
表达式,而是使用head
和tail
等部分函数。如果您尝试使用空列表调用其中任何一个,则会抛出异常。
> head []
*** Exception: Prelude.head: empty list
> tail []
*** Exception: Prelude.tail: empty list
如果在使用这些功能编写代码时出错,则在运行时才会检测到错误。如果你在模式匹配时出错,你的程序将无法编译。
例如,假设您不小心切换了函数的then
和else
部分。
-- 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
请注意,您的警卫示例存在同样的问题。
答案 1 :(得分:20)
我认为Boolean Blindness文章很好地回答了这个问题。问题是布尔值一旦构造就失去了所有的语义。这使得它们成为错误的重要来源,也使代码更难理解。
答案 2 :(得分:16)
您的第一个版本是您的教授首选版本,与其他版本相比具有以下优势:
null
head
和tail
。我认为这个被认为是“最佳实践”。
有什么大不了的?为什么我们要特别避免head
和tail
?好吧,每个人都知道这些功能并不完全,所以人们会自动尝试确保涵盖所有案例。 []上的模式匹配不仅突出超过null xs
,编译器可以检查一系列模式匹配的完整性。因此,具有完整模式匹配的惯用版本更容易掌握(对于训练有素的Haskell读取器)并且由编译器证明是穷举的。
第二个版本略好于最后一个版本,因为人们立即看到所有案例都得到处理。尽管如此,在一般情况下,第二个等式的RHS可能更长,并且可能存在带有几个定义的where子句,最后一个可能是这样的:
where
... many definitions here ...
head xs = ... alternative redefnition of head ...
要绝对明白RHS的作用,必须确保通用名称没有被重新定义。
第三个版本是最糟糕的一个恕我直言:a)第二场比赛未能解构列表仍然使用头尾。 b) case 比具有2个等式的等效符号稍微冗长。
答案 3 :(得分:8)
在许多编程语言中,if语句是基本原语,而switch-blocks之类的东西只是使深度嵌套的if语句更容易编写的语法糖。
Haskell反过来做了。模式匹配是基本原语,if-expression实际上只是模式匹配的语法糖。类似地,像null
和head
这样的构造只是用户定义的函数,它们最终都是使用模式匹配实现的。所以模式匹配就是最底层的事情。 (因此可能比调用用户定义的函数更有效。)
在许多情况下 - 例如你上面列出的那些 - 只是风格问题。编译器几乎可以肯定地将所有版本的性能大致相等。但是通常 [并不总是!]模式匹配使得它更准确地说明了你想要实现的目标。
(写一个if-expression非常容易,你可以用错误的方式得到两个替代方案。你认为这将是一个罕见的错误,但它出奇的常见。匹配,没有什么机会犯这个特定的错误,尽管还有很多其他事情要搞砸了。)
答案 4 :(得分:6)
每次调用null
,head
和tail
都需要进行模式匹配。但是你的答案中的第一个版本只进行了一次模式匹配,并通过模式的命名组件重用其结果。
为此,它更好。但它在视觉上更明显,更具可读性。
答案 5 :(得分:5)
由于(至少)以下原因,模式匹配优于if-then-else语句字符串:
模式匹配有助于减少代码中“意外复杂性”的数量 - 也就是说,代码实际上更多地是关于实现细节而不是程序的基本逻辑。
在大多数其他语言中,当compier / run-time看到一串if-then-else语句时,它别无选择,只能按照程序员指定的顺序测试条件。但模式匹配鼓励程序员更多地关注描述应该发生什么以及应该如何执行。由于Haskell中值的纯度和不变性,编译器可以将模式集合视为一个整体,并决定如何最好地实现它们。
类比将是C的switch
陈述。如果转储各种switch语句的汇编代码,您将看到有时编译器将生成一个链/树比较,而在其他情况下,它将生成一个跳转表。程序员在两种情况下都使用相同的语法 - 编译器根据比较值选择实现。如果它们形成连续的值块,则使用跳转表方法,否则使用比较树。如果检测到比较值中的其他模式,这种关注点的分离允许编译器在将来实施更多策略。